diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4409a95534..a851258417 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: pr: - description: Pull request number to build + description: Pull request numbers to build together, separated by spaces or commas (up to 8) required: true type: string linux: @@ -77,38 +77,51 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) { - throw new Error('Enter a positive pull request number'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas'); } } - const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number; - if (!Number.isSafeInteger(pull_number) || pull_number < 1) { + const numbers = manual + ? [...new Set(process.env.PR_NUMBER.trim().split(/[ ,]+/).map(Number))] + : [context.payload.pull_request.number]; + if (numbers.some((pull_number) => !Number.isSafeInteger(pull_number) || pull_number < 1)) { throw new Error('Invalid pull request number'); } - let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); - for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { - await new Promise((resolve) => setTimeout(resolve, 3000)); - ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); - } - if (pr.state !== 'open' || pr.base.ref !== 'main') { - throw new Error('The pull request must be open and target main'); - } - if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { - throw new Error('The pull request has newer commits; use the latest run'); - } - if (pr.mergeable === false) { - throw new Error('The pull request does not merge cleanly into main; rebase it first'); - } + const pulls = []; + const paths = []; + let truncated = false; let relinker = manual; - if (!manual) { - const { data: commits } = await github.rest.repos.listCommits({ - owner, repo, sha: 'main', author: pr.user.login, per_page: 1 - }); - relinker = commits.length > 0; + for (const pull_number of numbers) { + let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); + for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); + } + if (!manual && pr.state !== 'open') { + await core.summary.addRaw(`PR #${pull_number} was ${pr.merged ? 'merged' : 'closed'} before this run started: nothing to build`).write(); + return; + } + if (pr.state !== 'open' || pr.base.ref !== 'main') { + throw new Error(`Pull request #${pull_number} must be open and target main`); + } + if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { + throw new Error('The pull request has newer commits; use the latest run'); + } + if (pr.mergeable === false) { + throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); + } + if (!manual) { + const { data: commits } = await github.rest.repos.listCommits({ + owner, repo, sha: 'main', author: pr.user.login, per_page: 1 + }); + relinker = commits.length > 0; + } + const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); + truncated ||= files.length >= 3000; + paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + pulls.push({ number: pull_number, head: pr.head.sha }); } - const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); - const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); - const touches = (...prefixes) => files.length >= 3000 || + const touches = (...prefixes) => truncated || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; @@ -121,15 +134,16 @@ jobs: const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); - core.setOutput('pr', String(pull_number)); - core.setOutput('head', pr.head.sha); + core.setOutput('pr', pulls.map((pull) => pull.number).join(' ')); + core.setOutput('head', pulls.map((pull) => pull.head).join(' ')); core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); - core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); + const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`); + merged.forEach((line) => core.info(line)); await core.summary.addRaw( - `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + + `${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); @@ -167,9 +181,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - if: runner.os == 'Windows' id: toolchain uses: actions/cache@v6 @@ -227,7 +247,7 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true + submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -235,21 +255,28 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - uses: hendrikmuhs/ccache-action@v1.2.24 with: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - - name: Install lavapipe and the SDL audio and XRandR development files - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev + - name: Install lavapipe and the SDL audio, XRandR and Wayland development files + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - id: mesa run: | echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT" - echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" + grep -m1 '^model name' /proc/cpuinfo + echo "cpu=$(grep -E '^(vendor_id|cpu family|model|flags)[[:space:]]*:' /proc/cpuinfo | sort -u | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT" - uses: actions/cache/restore@v6 with: path: .mesa-shader-cache @@ -296,7 +323,7 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: true + submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -304,9 +331,15 @@ jobs: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | - git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" - if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi - if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done git submodule update --init --recursive - id: toolchain uses: actions/cache@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbab5a8f97..1cc1d9aa9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v7 with: - submodules: true + submodules: recursive - id: toolchain if: matrix.platform == 'windows' uses: actions/cache@v6 @@ -62,7 +62,7 @@ jobs: save: false - name: Install lavapipe for the GPU execution tests and the audio development files if: matrix.platform == 'linux' - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev - name: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE @@ -130,7 +130,12 @@ jobs: done python tools/package_release.py --docs docs/user --output dist chmod +x "dist/relinker-$RELEASE_TAG" + - name: Prepare release notes + shell: bash + run: | + gh api --method POST "repos/$GH_REPO/releases/generate-notes" -f tag_name="$RELEASE_TAG" > "$RUNNER_TEMP/release-notes.json" + python tools/prepare_release_notes.py --input "$RUNNER_TEMP/release-notes.json" --body "$RUNNER_TEMP/release-body.md" --full dist/release-notes.md - name: Create draft and upload files - run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes + run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-body.md" - name: Publish complete release run: gh release edit "$RELEASE_TAG" --draft=false diff --git a/.gitmodules b/.gitmodules index ddbcc22956..efcc5aa70b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "3rdparty/libusb-cmake"] path = 3rdparty/libusb-cmake url = https://github.com/libusb/libusb-cmake.git +[submodule "3rdparty/mbedtls"] + path = 3rdparty/mbedtls + url = https://github.com/Mbed-TLS/mbedtls.git diff --git a/3rdparty/mbedtls b/3rdparty/mbedtls new file mode 160000 index 0000000000..068ff080b3 --- /dev/null +++ b/3rdparty/mbedtls @@ -0,0 +1 @@ +Subproject commit 068ff080b369adfac81509f9b57b2afabaf82dc5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3eb8dd5333..a5ec80bd05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,8 @@ cmake_minimum_required(VERSION 3.20) +if(CMAKE_HOST_APPLE AND NOT ANYPS5_RELINKER_ONLY) + set(CMAKE_APPLE_SILICON_PROCESSOR x86_64 CACHE STRING "Host processor CMake reports on Apple silicon") + set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "Architectures built on macOS") +endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") @@ -61,6 +65,11 @@ elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() +if(APPLE AND NOT ANYPS5_RELINKER_ONLY) + add_compile_options($<$:-fexperimental-library>) + add_link_options(-fexperimental-library) +endif() + if(BUILD_TESTING) add_test_executable(shader_cache_file_read_tests ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp) @@ -68,6 +77,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler) add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests) set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20) + add_test_executable(request_serializer_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp) + target_include_directories(request_serializer_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include) + add_test(NAME request_serializer COMMAND request_serializer_tests) + set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} @@ -127,6 +144,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py) + add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py) endif() endif() @@ -135,13 +154,17 @@ if(ANYPS5_RELINKER_ONLY) return() endif() +if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") + message(FATAL_ERROR "The full build on macOS is x86-64 only; configure without CMAKE_OSX_ARCHITECTURES or set it to x86_64") +endif() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) set(SDL_VIDEO ON CACHE BOOL "" FORCE) set(SDL_RENDER OFF CACHE BOOL "" FORCE) set(SDL_X11 ON CACHE BOOL "" FORCE) -set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) +set(SDL_WAYLAND ON CACHE BOOL "" FORCE) set(SDL_OPENGL OFF CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_VULKAN ON CACHE BOOL "" FORCE) diff --git a/README.md b/README.md index a585752d17..b07e1abc67 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Tool for automatic executables porting to Linux and Windows. -[**Online converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. +[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. + +![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994) [Discord](https://discord.gg/BHFztBPUe) diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index fcb338b8c0..f16c5df8a6 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -16,6 +16,12 @@ function(add_jpeg_decoder target runtimeTarget) set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a") endif() file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include") + set(jpeg_turbo_platform_args) + if(APPLE) + list(APPEND jpeg_turbo_platform_args + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -DCMAKE_APPLE_SILICON_PROCESSOR=${CMAKE_APPLE_SILICON_PROCESSOR}) + endif() ExternalProject_Add(jpeg_turbo_external SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo" BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build" @@ -38,6 +44,7 @@ function(add_jpeg_decoder target runtimeTarget) -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + ${jpeg_turbo_platform_args} BUILD_BYPRODUCTS "${jpeg_turbo_library}" ) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 0093e1ffe2..2b49528bdd 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -9,6 +9,12 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() +if(APPLE) + set(ANYPS5_PRX_RPATH "@loader_path") +else() + set(ANYPS5_PRX_RPATH "$ORIGIN") +endif() + add_executable(nid_patcher ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp @@ -76,7 +82,7 @@ set_target_properties(libc PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) @@ -131,7 +137,7 @@ foreach(LIB_DIR ${LIBS_DIRS}) LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} BUILD_WITH_INSTALL_RPATH ON - INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH "${ANYPS5_PRX_RPATH}" ) set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx) @@ -209,6 +215,10 @@ if(BUILD_TESTING) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) endif() add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) + endif() add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp) target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) @@ -236,6 +246,14 @@ if(BUILD_TESTING) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) endif() + if(WIN32) + add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp) + add_dependencies(undeliverable_host_exception_tests libc) + add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $) + set_tests_properties(undeliverable_host_exception PROPERTIES + PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") + endif() + add_test_executable(guest_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) @@ -437,6 +455,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(MemoryPoolTest + tests/MemoryPoolTest.cpp + prx/libkernel/DirectMemory/MemoryPool.cpp + ) + target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(MINGW) + target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3f73232949..9ddb8391f4 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1181,7 +1181,7 @@ struct ImeEvent { ImeEventParam param; }; -using EventHandler = void (*)(void* arg, const ImeEvent* event); +using EventHandler = void (APS5_VABI *)(void* arg, const ImeEvent* event); struct Param { std::int32_t user_id; diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index e97b8bfcb2..b5470147d9 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -15,6 +15,11 @@ namespace Nid { namespace { +void RequireElfRange(const std::vector& elf, std::uint64_t offset, std::uint64_t size, const char* table) { + if (offset > elf.size() || size > elf.size() - offset) + throw std::runtime_error(std::string("invalid ELF ") + table + " range"); +} + std::uint32_t GnuHash(const std::string& name) { std::uint32_t h = 5381u; for (const unsigned char c : name) @@ -317,6 +322,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& if (elf[4] != 2) throw std::runtime_error("not ELF64"); const auto ehdr = Read(elf, 0u); + if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr)) + throw std::runtime_error("unsupported ELF byte order or section header size"); + RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table"); std::size_t dynSymOffset = 0u, dynSymSize = 0u; std::size_t gnuHashOffset = 0u, gnuHashSize = 0u; @@ -327,6 +335,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); if (shdr.sh_type == kShtDynsym) { + if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum) + throw std::runtime_error("invalid ELF dynsym layout"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym"); dynSymOffset = static_cast(shdr.sh_offset); dynSymSize = static_cast(shdr.sh_size); dynSymSectionIndex = i; @@ -363,6 +374,8 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& { const std::size_t shOffset = static_cast(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr); const auto shdr = Read(elf, shOffset); + if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type"); + RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr"); dynStrOffset = static_cast(shdr.sh_offset); dynStrSize = static_cast(shdr.sh_size); } diff --git a/core/libs/nid/tests/test_elf_layout.py b/core/libs/nid/tests/test_elf_layout.py new file mode 100644 index 0000000000..703ac46db3 --- /dev/null +++ b/core/libs/nid/tests/test_elf_layout.py @@ -0,0 +1,48 @@ +import struct +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, symbols, patch, SHOFF + + +class ElfLayoutTests(unittest.TestCase): + def test_declared_layout_must_match_supported_tables(self): + changes = ((5, "B", 2), (0x3a, "H", 32), + (SHOFF + 64 + 56, "Q", 8), + (SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1), + (SHOFF + 128 + 4, "I", 1)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def test_table_ranges_and_links_are_checked(self): + changes = ((0x28, "Q", 0x10000), + (SHOFF + 64 + 24, "Q", 0xff0), + (SHOFF + 64 + 40, "I", 100), + (SHOFF + 128 + 24, "Q", 0xff0), + (SHOFF + 128 + 32, "Q", 0xffffffffffffffff)) + for offset, kind, value in changes: + with self.subTest(offset=offset, value=value): + image = build_image(symbols(False), False) + struct.pack_into("<" + kind, image, offset, value) + self.reject(image) + + def reject(self, image): + result, after = patch(PATCHER, bytes(image), "layout.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr") + self.assertEqual(after, image) + + def test_valid_supported_layout_still_patches(self): + image = build_image(symbols(False), False) + result, after = patch(PATCHER, bytes(image), "valid-layout.so") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertNotEqual(after, image) + + +if __name__ == "__main__": + PATCHER = Path(sys.argv.pop(1)).resolve() + unittest.main() diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index e7271811c2..96c45f0d3c 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev Agc::Command::CheckAddress(guestAddress, 8, __func__); return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast(guestAddress), static_cast(guestAddress >> 32u)}, __func__); } - Agc::Command::Require(address == nullptr, __func__, "this event does not use an address"); const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u; return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__); } diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 499217f13b..8bae004ff1 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,8 +31,7 @@ constexpr std::uint32_t SCodeEnd = 0xbf9f0000u; constexpr std::uint32_t SNop = 0xbf800000u; constexpr std::uint32_t SSetpcS6 = 0xbe802006u; constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u; -constexpr std::uint8_t FusedCodeAlignmentLog2 = 8; -constexpr std::size_t FusedCodeAlignment = std::size_t{1} << FusedCodeAlignmentLog2; +constexpr std::size_t FusedCodeAlignment = 256; constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'}; constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t); constexpr std::uint32_t Rsrc1VgprMask = 0x3fu; @@ -191,14 +192,45 @@ void SetProgramAddress(std::vector& regs, std::uint32_t loOffset throw std::runtime_error("sceAgcFuseShaderHalves_0200: back half has no program address register pair"); } -int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { - constexpr auto fn = "sceAgcFuseShaderHalves_0200"; - if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); - auto layout = ComputeGeometryLayout(fn, front, back); - const auto scratch = reinterpret_cast(scratch_mem); - const auto base = AlignUp(scratch, FusedCodeAlignment); +struct FusedProgram { + std::vector storage; + Shader* header; +}; + +std::mutex FusedProgramsMutex; +std::map, FusedProgram> FusedPrograms; + +void AppendBytes(std::vector& bytes, const void* data, std::size_t size) { + if (size == 0) return; + const auto* begin = static_cast(data); + bytes.insert(bytes.end(), begin, begin + size); +} + +std::vector FusionKey(const Shader* front, const Shader* back, const GeometryLayout& layout) { + std::vector key; + constexpr auto headerBytes = offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers); + AppendBytes(key, front, headerBytes); + AppendBytes(key, back, headerBytes); + AppendBytes(key, const_cast(front->code), layout.frontBytes); + AppendBytes(key, const_cast(back->code), back->shader_size); + AppendBytes(key, layout.sh.data(), layout.sh.size() * sizeof(ShaderRegister)); + AppendBytes(key, layout.cx.data(), layout.cx.size() * sizeof(ShaderRegister)); + AppendBytes(key, back->specials, back->special_sizes_bytes); + AppendBytes(key, front->input_semantics, front->num_input_semantics * sizeof(ShaderSemantic)); + AppendBytes(key, back->output_semantics, back->num_output_semantics * sizeof(ShaderSemantic)); + if (back->user_data == nullptr) return key; + AppendBytes(key, back->user_data, offsetof(ShaderUserData, sharp_resource_count) + sizeof(ShaderUserData::sharp_resource_count)); + AppendBytes(key, back->user_data->direct_resource_offset, back->user_data->direct_resource_count * sizeof(std::uint16_t)); + for (std::size_t i = 0; i < layout.sharpOffsets.size(); ++i) AppendBytes(key, back->user_data->sharp_resource_offset[i], back->user_data->sharp_resource_count[i] * sizeof(ShaderSharp)); + return key; +} + +FusedProgram BuildFusedProgram(const char* fn, const Shader* front, const Shader* back, GeometryLayout& layout) { + FusedProgram program{std::vector(layout.totalBytes + FusedCodeAlignment - 1), nullptr}; + const auto storage = reinterpret_cast(program.storage.data()); + const auto base = AlignUp(storage, FusedCodeAlignment); if ((base & SHADER_BASE_ALIGN_MASK) != 0) Fail(fn, "fused shader memory is not a valid program address"); - auto* memory = static_cast(scratch_mem) + (base - scratch); + auto* memory = program.storage.data() + (base - storage); std::memcpy(memory, const_cast(front->code), layout.frontBytes); for (std::size_t offset = layout.frontBytes; offset < layout.backOffset; offset += sizeof(SNop)) std::memcpy(memory + offset, &SNop, sizeof(SNop)); std::memcpy(memory + layout.backOffset, const_cast(back->code), back->shader_size); @@ -241,10 +273,24 @@ int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* fused.type = static_cast(ShaderBinaryType::Gs); fused.num_sh_registers = static_cast(layout.sh.size()); fused.num_cx_registers = static_cast(layout.cx.size()); - auto* header = reinterpret_cast(memory + layout.headerOffset); - *header = fused; + program.header = reinterpret_cast(memory + layout.headerOffset); + *program.header = fused; + AgcDriverRegisterShader_nid_postfix(program.header); + return program; +} + +int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { + constexpr auto fn = "sceAgcFuseShaderHalves_0200"; + if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory"); + auto layout = ComputeGeometryLayout(fn, front, back); + auto key = FusionKey(front, back, layout); + std::lock_guard lock(FusedProgramsMutex); + auto found = FusedPrograms.find(key); + if (found == FusedPrograms.end()) found = FusedPrograms.emplace(std::move(key), BuildFusedProgram(fn, front, back, layout)).first; + Shader fused = *found->second.header; + std::memcpy(scratch_mem, fused.sh_registers, std::size_t{fused.num_sh_registers} * sizeof(ShaderRegister)); + fused.sh_registers = static_cast(scratch_mem); *fused_result = fused; - AgcDriverRegisterShader_nid_postfix(header); return 0; } @@ -306,8 +352,8 @@ int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; if (GeometryHalves(front)) { const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back); - dst->m_size = layout.totalBytes + FusedCodeAlignment - 1; - dst->m_align = FusedCodeAlignmentLog2; + dst->m_size = layout.sh.size() * sizeof(ShaderRegister); + dst->m_align = 4; return 0; } dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 03aeff9061..6fc0d679e7 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -10,6 +10,8 @@ #include #include +extern "C" std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t eventType, const volatile void* address); +extern "C" std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType); extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state); 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); @@ -641,6 +643,47 @@ void testDefaults() { expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } +void testEventWrite() { + Storage storage; + const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1000)); + check(samplePacket == storage.words.data(), "incorrect event 0x38 return pointer"); + check(storage.words[0] == 0xc0024600u, "incorrect event 0x38 packet header"); + check(storage.words[1] == (0x100u | 0x38u), "incorrect event 0x38 first payload word"); + check(storage.words[2] == 0x1000u, "incorrect event 0x38 low address word"); + check(storage.words[3] == 0u, "incorrect event 0x38 high address word"); + check(sceAgcDcbEventWriteGetSize(0x38) == 16, "incorrect event 0x38 size"); + check(sceAgcDcbEventWriteGetSize(0x39) == 16, "incorrect event 0x39 size"); + + Storage sample39; + sceAgcDcbEventWrite(&sample39.buffer, 0x39, reinterpret_cast(0x2008)); + check(sample39.words[0] == 0xc0024600u && sample39.words[1] == (0x100u | 0x39u) && sample39.words[2] == 0x2008u && sample39.words[3] == 0u, "incorrect event 0x39 packet"); + + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast(0x1001)); }); + expectFailure([&] { sceAgcDcbEventWrite(&storage.buffer, 0x40, nullptr); }); + + Storage partialFlush; + sceAgcDcbEventWrite(&partialFlush.buffer, 7, nullptr); + check(partialFlush.words[0] == 0xc0004600u && partialFlush.words[1] == 0x407u, "incorrect event 7 partial flush packet"); + check(sceAgcDcbEventWriteGetSize(7) == 8, "incorrect event 7 size"); + + Storage partialFlush15; + sceAgcDcbEventWrite(&partialFlush15.buffer, 15, nullptr); + check(partialFlush15.words[0] == 0xc0004600u && partialFlush15.words[1] == 0x40fu, "incorrect event 15 partial flush packet"); + + Storage partialFlush16; + sceAgcDcbEventWrite(&partialFlush16.buffer, 16, nullptr); + check(partialFlush16.words[0] == 0xc0004600u && partialFlush16.words[1] == 0x410u, "incorrect event 16 partial flush packet"); + + Storage cacheFlushNull; + sceAgcDcbEventWrite(&cacheFlushNull.buffer, 0x16, nullptr); + check(cacheFlushNull.words[0] == 0xc0004600u && cacheFlushNull.words[1] == 0x16u, "incorrect event 0x16 null address packet"); + check(sceAgcDcbEventWriteGetSize(0x16) == 8, "incorrect event 0x16 size"); + + Storage cacheFlushWithAddress; + sceAgcDcbEventWrite(&cacheFlushWithAddress.buffer, 0x16, reinterpret_cast(0x2d2a10000ull)); + check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet"); +} + } int main(int argc, char** argv) { @@ -652,6 +695,7 @@ int main(int argc, char** argv) { return 0; } testPackets(); + testEventWrite(); testNop(); testClearState(); testIndexedIndirectDraws(); diff --git a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp index c0a6a4eda6..2c901efc5b 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderFusion.cpp @@ -129,21 +129,20 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { Halves halves(terminator); SizeAlign size{}; check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed"); - check(size.m_align == 8, "fused shaders are not 256-byte aligned"); - check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header"); + check(size.m_size == 7 * sizeof(ShaderRegister) && size.m_align == 4, "fused size is not the fused register list"); std::vector storage(size.m_size + 512, 0xcd); auto* scratch = aligned(storage, 256) + misalignment; Shader fused{}; check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed"); for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size"); - auto* memory = scratch + (256 - misalignment) % 256; - const auto base = reinterpret_cast(memory); - const auto scratchBase = reinterpret_cast(scratch); + const auto base = reinterpret_cast(fused.code); + const auto* memory = reinterpret_cast(base); check(fused.type == static_cast(ShaderRegs::ShaderBinaryType::Gs), "fused shader is not a GS"); - check(fused.code == memory && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); + check(base % 256 == 0 && fused.shader_size == 256 + halves.back.shader_size, "fused code placement is wrong"); check(word(memory, 0) == SMovS0 && word(memory, 1) == VMovV0, "front program was not copied"); for (std::size_t i = 2; i < 64; ++i) check(word(memory, i) == SNop, "front program does not fall through to the back half"); check(std::memcmp(memory + 256, halves.backCode.data(), halves.back.shader_size) == 0, "back program was not copied"); + check(reinterpret_cast(fused.sh_registers) == reinterpret_cast(scratch), "fused registers are not in the caller's memory"); const auto* backLo = find(fused, 0x088u); const auto* frontLo = find(fused, 0x0C8u); check(backLo != nullptr && backLo->value == static_cast((base + 256) >> 8u), "back program address was not patched"); @@ -158,17 +157,42 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) { check(fused.num_output_semantics == 1 && fused.output_semantics[0].semantic == 3, "back outputs were not kept"); check(fused.scratch_size_dw_per_thread == 16, "scratch size is not the larger half's"); check(fused.specials != nullptr && fused.specials != halves.back.specials && fused.specials->ge_cntl.value == 0x77u, "back specials were not copied"); - const auto inside = [&](const void* pointer) { - const auto address = reinterpret_cast(pointer); - return address >= scratchBase && address < scratchBase + size.m_size; + const auto inside = [&](std::uintptr_t address) { + const auto begin = reinterpret_cast(storage.data()); + return address >= begin && address < begin + storage.size(); }; - check(inside(fused.sh_registers) && inside(fused.cx_registers) && inside(fused.specials) && inside(fused.input_semantics) && inside(fused.output_semantics), "fused tables are not in the fused memory"); - check(inside(fused.user_data) && inside(fused.user_data->direct_resource_offset) && inside(fused.user_data->sharp_resource_offset[1]), "fused user data is not in the fused memory"); + const auto program = [&](const void* pointer) { + const auto address = reinterpret_cast(pointer); + return !inside(address) && address >= base + fused.shader_size && address < base + fused.shader_size + fused.header_size + alignof(Shader); + }; + check(!inside(base) && program(fused.cx_registers) && program(fused.specials) && program(fused.input_semantics) && program(fused.output_semantics), "fused tables are not beside the fused code"); + check(program(fused.user_data) && program(fused.user_data->direct_resource_offset) && program(fused.user_data->sharp_resource_offset[1]), "fused user data is not beside the fused code"); check(fused.user_data->direct_resource_count == 2 && fused.user_data->direct_resource_offset[1] == 9u && fused.user_data->sharp_resource_offset[1][0].offset_dw == 12 && fused.user_data->sharp_resource_offset[0] == nullptr && fused.user_data->eud_size_dw == 4, "fused user data is wrong"); - const auto headerOffset = static_cast(size.m_size) - 255 - fused.header_size; - check(headerOffset % alignof(Shader) == 0 && headerOffset >= fused.shader_size, "fused header is misplaced"); - const auto* header = reinterpret_cast(memory + headerOffset); - check(header->code == fused.code && header->sh_registers == fused.sh_registers && header->user_data == fused.user_data && header->header_size == fused.header_size && header->shader_size == fused.shader_size && header->type == fused.type && header->num_sh_registers == fused.num_sh_registers, "fused memory has no self-contained header"); +} + +void testScratchReuse() { + Halves first; + Halves second; + second.backCode[0] = VMovV0; + SizeAlign size{}; + check(sceAgcGetFusedShaderSize_0080(&size, &first.front, &first.back) == 0, "fused size query failed"); + std::vector scratch(size.m_size); + Shader firstFused{}; + Shader secondFused{}; + check(sceAgcFuseShaderHalves_0200(&firstFused, &first.front, &first.back, scratch.data()) == 0, "first fusion failed"); + const auto firstCode = reinterpret_cast(firstFused.code); + check(sceAgcFuseShaderHalves_0200(&secondFused, &second.front, &second.back, scratch.data()) == 0, "second fusion failed"); + const auto secondCode = reinterpret_cast(secondFused.code); + check(firstCode != secondCode, "two fused programs share one code address"); + check(word(reinterpret_cast(firstCode), 64) == SMovS0, "fusing into reused memory overwrote the first fused program"); + check(word(reinterpret_cast(secondCode), 64) == VMovV0, "second fused program has the wrong back half"); + Shader again{}; + check(sceAgcFuseShaderHalves_0200(&again, &first.front, &first.back, scratch.data()) == 0, "repeated fusion failed"); + check(reinterpret_cast(again.code) == firstCode, "the same halves were fused into a second program"); + first.backCode[0] = VMovV0; + Shader changed{}; + check(sceAgcFuseShaderHalves_0200(&changed, &first.front, &first.back, scratch.data()) == 0, "fusion of changed halves failed"); + check(reinterpret_cast(changed.code) != firstCode && word(reinterpret_cast(firstCode), 64) == SMovS0, "changed halves reused the earlier fused program"); } void testRejections() { @@ -252,6 +276,7 @@ int main() { testFusion(0xa0); testFusion(4); testFusion(0, SSwappcNullS6); + testScratchReuse(); testRejections(); testHullHalves(); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..3ea2ccf2c8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -108,6 +108,23 @@ add_custom_command( DEPENDS "${agcSampleDumpsShaderSource}" glslang-standalone VERBATIM ) +foreach(agcPresentationStage IN ITEMS Vert Frag) + string(TOLOWER "${agcPresentationStage}" agcPresentationExtension) + string(TOUPPER "${agcPresentationStage}" agcPresentationSymbol) + set(agcPresentationShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PresentationPass.${agcPresentationExtension}) + set(agcPresentationShaderSpv ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}.spv) + set(agcPresentationShaderHeader ${agcTextureDetileGeneratedDir}/PresentationPass${agcPresentationStage}_spv.h) + add_custom_command( + OUTPUT "${agcPresentationShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcPresentationShaderSpv}" "${agcPresentationShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcPresentationShaderSpv} -DOUTPUT=${agcPresentationShaderHeader} + -DSYMBOL=PRESENTATION_PASS_${agcPresentationSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcPresentationShaderSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -174,6 +191,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.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 @@ -210,6 +228,7 @@ set(agcDriverSources ${agcTextureDetileImageShaderHeader} ${agcSampleCounterShaderHeader} ${agcSampleDumpsShaderHeader} + ${agcPresentationShaderHeaders} ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Eq/src/Query.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Resource/src/Lifecycle.cpp @@ -618,6 +637,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_redundant_value_elimination_tests EXCLUDE_FROM_ALL tests/RedundantValueElimination.cpp) +target_link_libraries(agc_redundant_value_elimination_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_redundant_value_elimination_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_redundant_value_elimination_tests) +if(BUILD_TESTING) + set_target_properties(agc_redundant_value_elimination_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_redundant_value_elimination COMMAND agc_redundant_value_elimination_tests) + if(WIN32) + set_tests_properties(agc_redundant_value_elimination PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp) 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) @@ -969,6 +1001,11 @@ if(BUILD_TESTING) set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + if(WIN32) + add_test(NAME agc_storage_cpu_snapshot COMMAND agc_driver_recorder_tests --storage-cpu-snapshot-only) + set_tests_properties(agc_storage_cpu_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) @@ -976,9 +1013,33 @@ if(BUILD_TESTING) add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) - set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_pending_write_snapshot COMMAND agc_driver_recorder_tests --pending-write-snapshot-only) + set_tests_properties(agc_pending_write_snapshot PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_write_snapshot PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_resident_present COMMAND agc_driver_recorder_tests --resident-present-only) + set_tests_properties(agc_resident_present PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_queued_write_groups COMMAND agc_driver_recorder_tests --queued-write-groups-only) + add_test(NAME agc_slice_dirty COMMAND agc_driver_recorder_tests --slice-dirty-only) + set_tests_properties(agc_slice_dirty PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only) + set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only) + set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_pending_key_stores COMMAND agc_driver_recorder_tests --pending-key-stores-only) + set_tests_properties(agc_pending_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_shadowed_key_stores COMMAND agc_driver_recorder_tests --shadowed-key-stores-only) + set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -1011,6 +1072,49 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_host_import_sparse_tests EXCLUDE_FROM_ALL tests/HostImportSparseDmaBuf.cpp) +target_include_directories(agc_host_import_sparse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_host_import_sparse_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_host_import_sparse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_host_import_sparse_tests) +if(BUILD_TESTING) + set_target_properties(agc_host_import_sparse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_host_import_sparse COMMAND agc_host_import_sparse_tests) + set_tests_properties(agc_host_import_sparse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_host_import_sparse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(agc_driver_presentation_pass_tests EXCLUDE_FROM_ALL + tests/PresentationPass.cpp +) + +target_include_directories(agc_driver_presentation_pass_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) + +target_link_libraries(agc_driver_presentation_pass_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) + +set_target_properties(agc_driver_presentation_pass_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_presentation_pass_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_presentation_pass_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_presentation_pass COMMAND agc_driver_presentation_pass_tests) + set_tests_properties(agc_driver_presentation_pass PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_presentation_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp ) @@ -1152,6 +1256,24 @@ target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +if(BUILD_TESTING) + add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + set_tests_properties(agc_driver_undithered_alpha_to_coverage_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 + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8) + 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 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $) @@ -1214,8 +1336,12 @@ if(BUILD_TESTING) add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_layout_pair COMMAND agc_shader_memory_tests --layout-pair) + add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) + set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS 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_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}) 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") @@ -1230,6 +1356,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) + set_tests_properties(agc_driver_partially_resident_pending_stores 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_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) @@ -1300,8 +1431,12 @@ if(BUILD_TESTING) set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests) set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_forced_fallback COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_forced_fallback COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FORCE_STRUCTURIZER_FALLBACK=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off agc_structurizer_forced_fallback agc_shader_control_flow_forced_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests) set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES @@ -1350,8 +1485,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) + 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}") if(WIN32) - set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_pm4_failure agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(NOT WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp index 899822c384..6ea0e234ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp @@ -4,6 +4,7 @@ #define VK_NO_PROTOTYPES #include #include +#include namespace AgcDriver { @@ -11,10 +12,12 @@ VkFormat DisplayTexelFormat(std::uint64_t pixelFormat); bool DisplayTenBit(std::uint64_t pixelFormat); bool DisplayRedLow(std::uint64_t pixelFormat); bool DisplayHalfFloat(std::uint64_t pixelFormat); +bool DisplayPq(std::uint64_t pixelFormat); std::uint32_t DisplayTexelBytes(std::uint64_t pixelFormat); enum class ResidentPresent : std::uint8_t { None, Blit, Convert }; ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, bool blitSource); +std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index 3c0093fbe8..3667d7bfd1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -8,6 +8,8 @@ namespace AgcDriver::DriverDetail { double TraceMs(); +std::uint64_t SkippedShader(); + 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/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 5fa2834621..83797987c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -23,6 +23,7 @@ struct DrawProgram { std::shared_ptr snapshot; std::size_t codeOffset = 0; + std::shared_ptr tailContinuation; }; struct DrawDecode { @@ -75,6 +76,19 @@ struct DrawEntryCounters { std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; +class DrawKeySightings { +public: + static constexpr std::size_t Ways = 4; + explicit DrawKeySightings(std::size_t keys); + bool SeenBefore(std::uint64_t key); + +private: + std::vector keys; + std::vector filled; + std::vector next; + std::size_t bucketMask = 0; +}; + enum class DrawVerdict { Drawn, Nothing, Rejected }; struct StageCapture { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ce483328b3..b3aa361927 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -115,6 +115,7 @@ private: static std::size_t drawCacheEntries(); void accountDrawVariant(const DispatchVariant& variant, bool added); void insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode); + bool admitDrawKey(std::uint64_t key); void eraseDrawEntry(std::uint64_t key, const std::shared_ptr& expected); std::shared_ptr findDrawRecipe(std::uint64_t key, const std::vector>& stages); void attachDrawRecipe(std::uint64_t key, const std::vector>& stages, std::shared_ptr recipe); @@ -125,7 +126,7 @@ private: std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); void resolveDrawDecode(const QueueState& queue, const Submission& submission, 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); - ShaderRecompiler::RecompileResult 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); 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); static bool drawPrecheck(); std::optional precheckDraw(const QueueState& queue, const Submission& submission, std::span packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); @@ -310,6 +311,7 @@ private: std::list drawOrder; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; + std::optional drawKeySightings; DrawEntryCounters drawEntryCounters; 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 025aa35c42..86e8d091a9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -2,6 +2,8 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_MEMORY_DWORDPATTERNFILL_HPP #include "Recompiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include #include #include @@ -19,6 +21,9 @@ struct DwordPatternFill { std::optional MatchDwordPatternFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute); std::optional MatchZeroDwordFill(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); +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/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 51334acc72..82c05c7ff9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -87,17 +87,19 @@ struct ShaderSnapshot { std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); std::shared_ptr> positionVectorUse = std::make_shared>(0u); + std::optional userDataTailRegister; }; std::shared_ptr ReadRawShader(std::uint64_t address); +std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); // The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program // without pos1..pos3 exports, so its artifact does not depend on them. -std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl); +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation = nullptr); bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 3932b5dc58..638dfe02d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -61,6 +61,7 @@ void WriteChanged(std::uint64_t address, std::span current, std bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); 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)); 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); @@ -72,6 +73,8 @@ struct UnchangedQuery { }; bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation); bool UnchangedSinceAll(std::span queries); +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges); +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation); // Returns the generation the blocks were stamped with (0 when the arena is not write-watched): // UnchangedSince(range, it) holds until the next store over the range. std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes); @@ -103,6 +106,7 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes); // collect stamps newer. std::uint64_t TrackerGeneration(); std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes); +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes); // Whether no CPU write touched the range since `generation`: an uncached resetting collect of the // range's pages first (a game store not collected yet becomes a block stamp), then the compare over // the stamps collects make (dirty pages), not the MarkWritten stamps of the driver's own GPU label diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index b859482795..0cefddf624 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -84,6 +84,44 @@ private: Clock::duration openedAt{}; }; +class TraceInterval { +public: + using Clock = std::chrono::steady_clock; + + static std::optional Parse(const char* seconds) { + if (seconds == nullptr) return std::nullopt; + const std::string_view text = seconds; + std::uint64_t length = 0; + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, length); + if (text.empty() || error != std::errc{} || last != end || length == 0 || length > 31536000) + throw std::invalid_argument("APS5_TRACE_SUMMARY_EVERY=" + std::string(text) + " is not a whole number of seconds above 0"); + return TraceInterval(length); + } + + bool Due(Clock::duration elapsed) const { + return elapsed - begun >= std::chrono::seconds(length); + } + + void Restart(Clock::duration elapsed) { + begun = elapsed; + } + + Clock::duration Begun() const { + return begun; + } + + std::string Describe() const { + return "summary_every " + std::to_string(length); + } + +private: + explicit TraceInterval(std::uint64_t length) : length(length) {} + + std::uint64_t length; + Clock::duration begun{}; +}; + class TraceThread { public: using Clock = std::chrono::steady_clock; @@ -111,8 +149,10 @@ public: bool dropped = false; }; - TraceThread(std::string name, const std::atomic& presents, std::size_t capacity = DefaultCapacity) - : name(std::move(name)), presents(presents), slices(std::make_unique(capacity)), capacity(capacity) {} + inline static const std::atomic AlwaysBuffering{true}; + + TraceThread(std::string name, const std::atomic& presents, std::size_t capacity = DefaultCapacity, const std::atomic& buffering = AlwaysBuffering) + : name(std::move(name)), presents(presents), buffering(buffering), slices(std::make_unique(capacity)), capacity(capacity) {} TraceThread(const TraceThread&) = delete; TraceThread& operator=(const TraceThread&) = delete; @@ -120,6 +160,7 @@ public: void Add(const char* scope, const char* stage, Clock::time_point begin, Clock::time_point end) { const auto length = end - begin; account(scope, stage, length); + if (!buffering.load(std::memory_order_relaxed)) return; const auto present = presents.load(std::memory_order_relaxed); if (!sampled || present != sampledPresent) { if (sampled) append({nullptr, nullptr, sampleBegin, folded}); @@ -182,6 +223,7 @@ private: const std::string name; const std::atomic& presents; + const std::atomic& buffering; const std::unique_ptr slices; const std::size_t capacity; std::atomic used{0}; @@ -206,6 +248,7 @@ struct TraceSummary { std::uint64_t firstPresent = 0; double gpuBusyMs = 0; std::string trace; + std::optional> interval; }; struct TraceGpuRange { @@ -307,6 +350,21 @@ public: return slices; } + static std::vector Since(std::span now, std::span before) { + std::vector result; + for (const auto& total : now) { + auto delta = total; + for (const auto& old : before) { + if (old.scope != total.scope || old.stage != total.stage) continue; + delta.length -= old.length; + delta.count -= old.count; + break; + } + if (delta.count != 0) result.push_back(delta); + } + return result; + } + static std::string Summary(const TraceSummary& summary, std::span threads, std::span presents) { const auto count = presents.empty() ? std::size_t{0} : presents.size() - 1; const auto span = presents.empty() ? Clock::duration{} : presents.back() - presents.front(); @@ -339,6 +397,7 @@ public: AppendString(output, summary.window); output += ",\"first_present\":" + std::to_string(summary.firstPresent); output += ",\"ms\":" + number(milliseconds(span)) + "}"; + if (summary.interval) output += ",\"interval_start_s\":" + number(summary.interval->first) + ",\"interval_end_s\":" + number(summary.interval->second); output += ",\"presents\":" + std::to_string(count); output += ",\"frame_ms\":{\"p50\":" + number(percentile(0.5)) + ",\"p90\":" + number(percentile(0.9)) + ",\"p99\":" + number(percentile(0.99)) + ",\"max\":" + number(frames.empty() ? 0.0 : frames.back()) + "}"; output += ",\"gpu_busy_ms_per_present\":" + number(summary.gpuBusyMs * perPresent); @@ -396,7 +455,7 @@ private: class PerformanceTrace { public: static bool Configured() { - static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr; return configured; } @@ -404,7 +463,7 @@ public: static void Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device); static void GpuRange(const char* name, double beginNs, double endNs) { - if (!Recording()) return; + if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { gpuDropped = true; @@ -424,7 +483,11 @@ public: static void StopOutput(); private: + static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present); + static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device); + inline static std::atomic recording{false}; + inline static std::atomic buffering{false}; inline static std::atomic presentCount{0}; inline static thread_local TraceThread* local = nullptr; inline static std::mutex gpuMutex; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp index e78777f28f..3786ec368b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp @@ -65,6 +65,7 @@ inline constexpr Opcode Opcodes[] = { {0x88, "WAIT_ON_DE_COUNTER_DIFF"}, {0x8D, "DISPATCH_DRAW"}, {0x8E, "GET_LOD_STATS"}, + {0x91, "INDEX_ATTRIBUTES_INDIRECT"}, {0x93, "WAIT_REG_MEM_64"}, {0x9F, "SET_CONTEXT_REG_INDIRECT"}, }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp new file mode 100644 index 0000000000..eca1e61a9e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp @@ -0,0 +1,78 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PQTOSDR_HPP + +#include +#include +#include +#include +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" + +namespace AgcDriver { + +inline constexpr float PqReferenceWhiteNits = 203.0f; +inline constexpr float PqToneMapKnee = 0.75f; +inline constexpr std::uint32_t PqCodes = 1024; + +inline const std::array& PqEotfNits() { + static const auto table = [] { + std::array nits{}; + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + for (std::uint32_t code = 0; code < PqCodes; ++code) { + const double e = std::pow(code / 1023.0, 1.0 / m2); + nits[code] = static_cast(10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1)); + } + return nits; + }(); + return table; +} + +inline float PqReferenceWhite(float whiteNits) { + return std::min(PqReferenceWhiteNits, whiteNits); +} + +inline float PqToneMap(float luminance, float relativeWhite) { + if (luminance <= PqToneMapKnee) return luminance; + const float t = (luminance - PqToneMapKnee) / (1.0f - PqToneMapKnee); + const float w = (relativeWhite - PqToneMapKnee) / (1.0f - PqToneMapKnee); + return PqToneMapKnee + (1.0f - PqToneMapKnee) * t * (1.0f + t / (w * w)) / (1.0f + t); +} + +inline std::array PqToSdrLinear(float red, float green, float blue, float whiteNits) { + const float referenceWhite = PqReferenceWhite(whiteNits); + const float r2020 = red / referenceWhite; + const float g2020 = green / referenceWhite; + const float b2020 = blue / referenceWhite; + std::array rgb{ + std::max(1.6604910f * r2020 - 0.5876411f * g2020 - 0.0728499f * b2020, 0.0f), + std::max(-0.1245505f * r2020 + 1.1328999f * g2020 - 0.0083494f * b2020, 0.0f), + std::max(-0.0181508f * r2020 - 0.1005789f * g2020 + 1.1187297f * b2020, 0.0f)}; + const float luminance = 0.2126f * rgb[0] + 0.7152f * rgb[1] + 0.0722f * rgb[2]; + if (luminance > 0.0f) { + const float scale = PqToneMap(luminance, whiteNits / referenceWhite) / luminance; + for (auto& channel : rgb) channel = std::min(channel * scale, 1.0f); + } + return rgb; +} + +inline std::uint8_t SrgbEncode8(float linear) { + const float v = std::clamp(linear, 0.0f, 1.0f); + const float encoded = v <= 0.0031308f ? 12.92f * v : 1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f; + return static_cast(std::floor(encoded * 255.0f + 0.5f)); +} + +inline std::array PqToSdrPixel(std::uint32_t word, bool redLow, float whiteNits) { + const auto& nits = PqEotfNits(); + const float low = nits[word & 0x3ffu]; + const float green = nits[(word >> 10u) & 0x3ffu]; + const float high = nits[(word >> 20u) & 0x3ffu]; + const auto rgb = PqToSdrLinear(redLow ? low : high, green, redLow ? high : low, whiteNits); + return {SrgbEncode8(rgb[2]), SrgbEncode8(rgb[1]), SrgbEncode8(rgb[0]), 0xff}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp new file mode 100644 index 0000000000..3bcf7f2085 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationPass.hpp @@ -0,0 +1,75 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATIONPASS_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +class PresentationPass { +public: + PresentationPass(const Graphics::Context& context, std::size_t slots); + ~PresentationPass(); + PresentationPass(const PresentationPass&) = delete; + PresentationPass& operator=(const PresentationPass&) = delete; + + void Bind(std::size_t slot, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer = 0); + void RecordPresent(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout); + void RecordDump(VkCommandBuffer commands, std::size_t slot, std::uint32_t stride, VkBuffer destination); + void Release(std::size_t slot) noexcept; + static bool TenBitFormat(VkFormat format); + static VkExtent2D DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride); + +private: + struct Target { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + }; + struct Slot { + VkDescriptorSet set = VK_NULL_HANDLE; + VkImageView source = VK_NULL_HANDLE; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t mode = 0; + float whiteNits = 0.0f; + std::vector> transient; + VkImage dumpImage = VK_NULL_HANDLE; + VkDeviceMemory dumpMemory = VK_NULL_HANDLE; + VkImageView dumpView = VK_NULL_HANDLE; + VkFramebuffer dumpFramebuffer = VK_NULL_HANDLE; + VkExtent2D dumpExtent{}; + }; + + const Target& target(VkFormat format, VkImageLayout finalLayout); + VkImageView createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer = 0) const; + VkFramebuffer createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const; + void ensureDumpTarget(Slot& slot, VkExtent2D extent); + void releaseDumpTarget(Slot& slot) noexcept; + void draw(VkCommandBuffer commands, const Slot& slot, const Target& target, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const; + void release() noexcept; + + Graphics::Context context; + VkSampler sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::unique_ptr pqNits; + std::map, Target> targets; + std::vector slots; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index 35e8ed497f..b0e88120aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -3,8 +3,10 @@ #define VK_NO_PROTOTYPES #include +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include +#include namespace AgcDriver { @@ -22,14 +24,16 @@ public: // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter); - void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height); + void RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer = 0); // Copies the source image's texels (tightly packed rows of its format) into a host-readable buffer // and makes them visible to the host once the submission completes. void RecordReadback(VkCommandBuffer commands, VkBuffer destination); void RecordBlit(VkCommandBuffer commands, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); // The aspect-fitting blit of RecordBlit from any image: presenting a resident render target needs // no copy into the source image first. - static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static void RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer = 0); + void RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight); + static std::vector LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight); std::uint32_t SourceWidth() const { return sourceWidth; } std::uint32_t SourceHeight() const { return sourceHeight; } @@ -42,6 +46,8 @@ private: std::uint32_t sourceHeight = 0; VkImage sourceImage = VK_NULL_HANDLE; VkDeviceMemory sourceMemory = VK_NULL_HANDLE; + VkImage blackImage = VK_NULL_HANDLE; + VkDeviceMemory blackMemory = VK_NULL_HANDLE; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index f270c94511..0d6a6da583 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -226,6 +226,7 @@ inline Registers BuildInitialContextRegisters() { {0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000}, {0x90, 0x80000000}, {0x91, 0x40004000}, {0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0}, + {0x10b, 0}, {0x10c, 0}, {0x10d, 0}, {0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0}, {0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0}, {0x2ce, 0}, {0x2d3, 0}, {0x2d5, 0}, {0x2d6, 0}, {0x2db, 0}, @@ -240,6 +241,7 @@ inline Registers BuildInitialContextRegisters() { result.emplace(0x31c + 0xfu * i, 0); } for (std::uint32_t i = 0; i < 16; ++i) { + result.emplace(0x2fe + i, 0); result.emplace(0x94 + 2 * i, 0x80000000); result.emplace(0x95 + 2 * i, 0x40004000); result.emplace(0xb4 + 2 * i, 0); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 51c74e1a0a..9a4dc16db6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp @@ -84,6 +84,7 @@ struct DrawRecipe { std::optional masked; std::set fragmentOutputs; VkPipelineStageFlags shaderStages = 0; + bool hasDepth = false; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp new file mode 100644 index 0000000000..b00601533e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TONEMAPLUMINANCE_HPP + +namespace AgcDriver { + +inline constexpr float SdrToneMapLuminanceNits = 100.0f; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index bd48aed75c..5809ae0851 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -322,7 +322,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr); + 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); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index d59fe232b6..de10849034 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include @@ -58,7 +59,14 @@ void decodeHalfFloatTexel(const std::byte* texel, std::byte* pixel, bool redLow) pixel[3] = static_cast(Graphics::HalfFloatScanoutCode(halves[3], true)); } -void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit) { +void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit, bool pq) { + if (pq) { + std::uint32_t word = 0; + std::memcpy(&word, texel, 4); + const auto converted = PqToSdrPixel(word, rgba, SdrToneMapLuminanceNits); + for (std::size_t i = 0; i < converted.size(); ++i) pixel[i] = static_cast(converted[i]); + return; + } if (tenBit) { std::uint32_t word = 0; std::memcpy(&word, texel, 4); @@ -116,7 +124,7 @@ void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { require(buffer.width != 0 && buffer.height != 0 && buffer.width <= 16384 && buffer.height <= 16384, "VideoOut: invalid display buffer dimensions"); const bool halfFloat = DisplayHalfFloat(buffer.pixelFormat); - if (!halfFloat && baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { + if (!halfFloat && !DisplayPq(buffer.pixelFormat) && baseFormat(buffer.pixelFormat) != PixelFormatB8G8R8A8 && baseFormat(buffer.pixelFormat) != PixelFormatR8G8B8A8) { char message[96]; std::snprintf(message, sizeof(message), "VideoOut: unsupported display pixel format 0x%016llx", static_cast(buffer.pixelFormat)); throw std::runtime_error(message); @@ -157,7 +165,8 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa return pixels; } const auto blocksPerRow = (buffer.width + 127u) / 128u; - const bool rgba = baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8; + const bool rgba = DisplayRedLow(buffer.pixelFormat); + const bool pq = DisplayPq(buffer.pixelFormat); // Bit 56 marks the 10-bit variant: A2B10G10R10 with red in the low bits (the scanout draws render // COLOR_2_10_10_10 into it). Presented as 8-bit BGRA by dropping the low two bits of each channel. const bool tenBit = (buffer.pixelFormat & PixelFormatUnormBit) != 0; @@ -168,7 +177,7 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; - decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit); + decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit, pq); } } timing.Mark("detile_convert"); @@ -193,7 +202,7 @@ std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr throw std::runtime_error(message); } std::array pixel{}; - decodeTexel(texel.data(), pixel.data(), baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8, (buffer.pixelFormat & PixelFormatUnormBit) != 0); + decodeTexel(texel.data(), pixel.data(), DisplayRedLow(buffer.pixelFormat) && !DisplayHalfFloat(buffer.pixelFormat), (buffer.pixelFormat & PixelFormatUnormBit) != 0, DisplayPq(buffer.pixelFormat)); return pixel; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp index 7ca977ac58..34c6c08530 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayFormat.cpp @@ -7,6 +7,8 @@ constexpr std::uint64_t PixelFormatUnormBit = 0x0100000000000000ull; constexpr std::uint64_t PixelFormatR8G8B8A8 = 0x8000000022000000ull; constexpr std::uint64_t PixelFormatR16G16B16A16Float = 0xc001000622000000ull; constexpr std::uint64_t PixelFormatB16G16R16A16Float = 0xc001000600000000ull; +constexpr std::uint64_t PixelFormatR10G10B10A2Bt2100Pq = 0x8100070422000000ull; +constexpr std::uint64_t PixelFormatB10G10R10A2Bt2100Pq = 0x8100070400000000ull; } @@ -15,7 +17,11 @@ bool DisplayTenBit(std::uint64_t pixelFormat) { } bool DisplayRedLow(std::uint64_t pixelFormat) { - return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8 || pixelFormat == PixelFormatR16G16B16A16Float; + return (pixelFormat & ~PixelFormatUnormBit) == PixelFormatR8G8B8A8 || pixelFormat == PixelFormatR16G16B16A16Float || pixelFormat == PixelFormatR10G10B10A2Bt2100Pq; +} + +bool DisplayPq(std::uint64_t pixelFormat) { + return pixelFormat == PixelFormatR10G10B10A2Bt2100Pq || pixelFormat == PixelFormatB10G10R10A2Bt2100Pq; } bool DisplayHalfFloat(std::uint64_t pixelFormat) { @@ -51,4 +57,11 @@ ResidentPresent ResidentPresentPath(VkFormat storage, std::uint64_t pixelFormat, } } +std::optional ResidentPresentSlice(std::uint64_t imageAddress, std::uint64_t imageBytes, std::uint32_t slices, std::uint64_t displayAddress, std::uint64_t displayBytes) { + if (slices == 0 || displayBytes == 0 || imageBytes % slices != 0 || imageBytes / slices != displayBytes || displayAddress < imageAddress) return std::nullopt; + const auto offset = displayAddress - imageAddress; + if (offset % displayBytes != 0 || offset / displayBytes >= slices) return std::nullopt; + return static_cast(offset / displayBytes); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 59d038112b..3e80f40e9b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -9,6 +10,11 @@ double TraceMs() { return std::chrono::duration(std::chrono::steady_clock::now() - origin).count(); } +std::uint64_t SkippedShader() { + static const std::uint64_t skipped = [] { const char* text = std::getenv("APS5_SKIP_SHADER"); return text ? std::strtoull(text, nullptr, 16) : 0ull; }(); + return skipped; +} + 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 a6da3ceb55..7719b40f2d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -16,6 +16,7 @@ namespace AgcDriver::DriverDetail { void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); + if (address == SkippedShader() && address != 0) return; auto registeredShader = RegisteredProgram(*submission.shaders, address, {0}); if (!registeredShader) registeredShader = ReadRawShader(address); const auto& snapshot = *registeredShader; 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 e51868b6aa..8858b58f2c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -94,6 +95,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& graphics = decode->state; auto pixel = decode->pixel; std::vector programs = decode->programs; + if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; auto& words = programs.front().userData; @@ -145,7 +147,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); - if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207)) | graphics.layerExports; + if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports; }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); @@ -153,6 +155,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe } ShaderMemory shaderMemory(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter()); std::vector results; + std::vector> compiledStages; + compiledStages.reserve(programs.size()); std::vector stages; results.reserve(programs.size() + 2u); stages.reserve(programs.size()); @@ -204,7 +208,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe programResults[i] = matched[i]->compiled.get(); memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); } else { - resultIndex[i] = results.size(); if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { for (auto previous = i; previous-- > 0;) { if (programResults[previous] == nullptr) continue; @@ -212,9 +215,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe break; } } - results.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)); if (!rejected.empty()) return DrawVerdict::Rejected; - programResults[i] = &results.back(); + programResults[i] = compiledStages.back().get(); } const auto& result = *programResults[i]; if (i == 0 && drawParameters.indirect) { @@ -291,17 +294,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (drawParameters.indirect && indirectCpu) { const auto indirect = *drawParameters.indirect; - if (drawHit) { - - for (std::size_t i = 0; i < programs.size(); ++i) { - if (programResults[i] == nullptr) continue; - resultIndex[i] = results.size(); - results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); - for (auto& stage : stages) { - if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; - } - programResults[i] = &results[resultIndex[i]]; + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programResults[i] == nullptr) continue; + resultIndex[i] = results.size(); + results.push_back(ShaderRecompiler::RecompileResult(*programResults[i])); + for (auto& stage : stages) { + if (stage.program == programResults[i]) stage.program = &results[resultIndex[i]]; } + programResults[i] = &results[resultIndex[i]]; } recordQueuedLabelsBeforeRead(submission.queue); const auto readStart = std::chrono::steady_clock::now(); @@ -343,7 +343,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, 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, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 7844679fd2..bf7cf368bf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" #include +#include #include namespace AgcDriver::DriverDetail { @@ -56,6 +57,36 @@ void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) { } } +DrawKeySightings::DrawKeySightings(std::size_t keyCount) { + Graphics::Require(keyCount >= Ways, "draw key sightings need at least one bucket"); + const auto buckets = std::bit_ceil(keyCount / Ways); + keys.assign(buckets * Ways, 0); + filled.assign(buckets, 0); + next.assign(buckets, 0); + bucketMask = buckets - 1; +} + +bool DrawKeySightings::SeenBefore(std::uint64_t key) { + auto mixed = key; + mixed = (mixed ^ (mixed >> 30u)) * 0xbf58476d1ce4e5b9ull; + mixed = (mixed ^ (mixed >> 27u)) * 0x94d049bb133111ebull; + mixed ^= mixed >> 31u; + const auto bucket = static_cast(mixed) & bucketMask; + auto* ways = keys.data() + bucket * Ways; + if (std::find(ways, ways + filled[bucket], key) != ways + filled[bucket]) return true; + ways[next[bucket]] = key; + next[bucket] = static_cast((next[bucket] + 1u) % Ways); + if (filled[bucket] < Ways) ++filled[bucket]; + return false; +} + +bool Driver::admitDrawKey(std::uint64_t key) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawCache.contains(key)) return true; + if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); + return drawKeySightings->SeenBefore(key); +} + void Driver::insertDrawEntry(std::uint64_t key, std::vector>& fresh, std::shared_ptr decode) { std::lock_guard cacheLock(drawCacheMutex); auto& counters = drawEntryCounters; @@ -120,7 +151,9 @@ void Driver::eraseDrawEntry(std::uint64_t key, const std::shared_ptr& bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry&) { return std::all_of(decode.programs.begin(), decode.programs.end(), [&](const DrawProgram& program) { - return !program.snapshot->header.empty() || ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot; + if (!program.snapshot->header.empty()) return true; + return ReadRawShader(program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t)) == program.snapshot && + (program.tailContinuation == nullptr || ReadRawShader(program.tailContinuation->codeAddress) == program.tailContinuation); }); } 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 3c6931fd2e..4f3db831ac 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -10,7 +10,7 @@ namespace AgcDriver::DriverDetail { -ShaderRecompiler::RecompileResult 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) { PerformanceTimer timing("Shader.DrawStage"); using Stage = ShaderRecompiler::ShaderStage; phaseTiming.Phase(DrawRowVectors); @@ -77,13 +77,12 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st timing.Mark("capture_resources"); stageCapture.compiled = invocation.Materialize(*capture); timing.Mark("materialize"); - ShaderRecompiler::RecompileResult result = *stageCapture.compiled; phaseTiming.Phase(DrawRowRecompile); - return result; + return stageCapture.compiled; } void Driver::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) { - if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu)) { + if (useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu) && (verifyHit || admitDrawKey(drawKey))) { phaseTiming.Phase(DrawRowVectors); std::uint64_t unstable = 0, mismatches = 0; for (std::size_t i = 0; i < programs.size(); ++i) { 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 80b56997d2..1168f3a115 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -51,6 +51,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i); result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i))); } + if (stage == Stage::Vertex && snapshot.header.empty() && snapshot.userDataTailRegister) result.tailContinuation = ReadUserDataTailContinuation(result.binary.code, result.firstUserSgpr, result.userData); return result; }; { @@ -117,6 +118,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.alphaToCoverage = product->state.alphaToCoverage; if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0; + product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage; return product; } 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 069e72aeb1..40b6eeef9d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -26,7 +26,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(tess.breakAtInstance)}) mix(value); } mix(graphics.rectList); mix(programs.size()); @@ -72,6 +72,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); + mix(pixel.ditheredAlphaToCoverage); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); 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 8369224ac7..af01cae687 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -118,7 +118,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; + 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; }; 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; @@ -137,9 +137,9 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { 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) return false; + 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.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false; return true; } 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 4d5f80f1f2..62c6153a13 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -14,13 +14,22 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool matchesBufferPatternFillKernel(std::span code) { + static constexpr std::array kernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + return code.size() >= kernel.size() && std::equal(kernel.begin(), kernel.end(), code.begin()); +} + +} + bool Driver::matchesFillKernel(std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; static const bool patternEnabled = std::getenv("APS5_NO_PATTERN_FILL_HLE") == nullptr; if (!enabled || userData.size() < 8 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return false; if (patternEnabled && MatchesPatternFillKernel(code, userData, compute)) return true; static constexpr std::array fillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; - if (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin())) return false; + if (!matchesBufferPatternFillKernel(code) && (code.size() < fillKernel.size() || !std::equal(fillKernel.begin(), fillKernel.end(), code.begin()))) return false; const auto stride = (userData[1] >> 16u) & 0x3fffu; const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; @@ -78,6 +87,32 @@ std::optional MatchZeroDwordFill(std::span(dwords * 4u), {}}; } +std::optional MatchMemoryValueBufferFill(std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) { + static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr; + if (!enabled || userData.size() != 8 || packet.size() < 5 || (packet[4] & 0x20u) != 0 || !compute.groupIdEnable[0]) return std::nullopt; + const auto fill = Graphics::MatchMemoryValueFill(code, userData, compute.numThreads, {packet[1], packet[2], packet[3]}); + if (!fill) return std::nullopt; + if (fill->address % 16u != 0 || fill->bytes % 16u != 0) return std::nullopt; + return fill; +} + +bool IsByteFill(const std::array& pattern) { + return std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); +} + +std::optional KeysFillMemo(const std::array& pattern, bool coversStorageKeys, bool memoryValueFill) { + if (!IsByteFill(pattern) || !(coversStorageKeys || memoryValueFill)) return std::nullopt; + switch (pattern[0] & 0xffu) { + case 0x00: return Graphics::DccKeys::Clear0000; + case 0x40: return Graphics::DccKeys::Clear0001; + case 0x80: return Graphics::DccKeys::Clear1110; + case 0xc0: return Graphics::DccKeys::Clear1111; + case 0x20: return Graphics::DccKeys::ClearRegister; + case 0xff: return Graphics::DccKeys::Uncompressed; + default: return std::nullopt; + } +} + bool Driver::fillClearEnabled() { static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr; return enabled; @@ -145,7 +180,9 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span packet, std::span code, const std::vector& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr& localDevice) { auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute); if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute); - if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false; + std::optional memoryFill; + if (!dwordFill) memoryFill = MatchMemoryValueBufferFill(packet, code, userData, compute); + if (!dwordFill && !memoryFill && !matchesFillKernel(code, userData, compute)) return false; std::uint64_t base; std::size_t bytes; std::array pattern; @@ -154,6 +191,15 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanbase; bytes = dwordFill->bytes; pattern = dwordFill->pattern; + } else if (memoryFill) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(memoryFill->valueAddress), 4)) return false; + GuestMemory::Read(memoryFill->valueAddress, std::as_writable_bytes(std::span(pattern).first(1)), 4); + std::fill(pattern.begin() + 1, pattern.end(), pattern[0]); + base = memoryFill->address; + bytes = memoryFill->bytes; } else if (MatchesPatternFillKernel(code, userData, compute)) { const auto range = DecodePatternFillRange(userData, packet); if (!range) return false; @@ -185,7 +231,16 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(static_cast(groups[0]) * 64u, numRecords); base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); bytes = static_cast(records * 16u); - pattern = {userData[4], userData[5], userData[6], userData[7]}; + if (matchesBufferPatternFillKernel(code)) { + const auto source = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + inputLock.lock(); + recordLabelsForPacket(localDevice.get(), queueId); + if (!GuestMemory::Accessible(reinterpret_cast(source), sizeof(pattern))) return false; + GuestMemory::Read(source, std::as_writable_bytes(std::span(pattern)), 4); + } else { + pattern = {userData[4], userData[5], userData[6], userData[7]}; + } } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static std::atomic fills{0}, filledBytes{0}, cpuFills{0}; @@ -197,7 +252,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span fillPhaseMs{}; static std::array holdByCoverMs{}; static std::array holdByCover{}; - static std::uint64_t flushes = 0, flushed = 0; + static std::uint64_t flushes = 0, flushed = 0, absorbedFills = 0, absorbedBytes = 0; static auto fillReport = std::chrono::steady_clock::now(); ++fills; filledBytes += bytes; @@ -222,7 +277,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(base), bytes, pattern[0], pattern[1], pattern[2], pattern[3], queueId); - const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && std::all_of(pattern.begin(), pattern.end(), [&](std::uint32_t word) { return word == (pattern[0] & 0xffu) * 0x01010101u; }); + const bool uniformKeysFill = coverage.cover == Graphics::StorageTexture::FillCover::Keys && IsByteFill(pattern); if (uniformKeysFill) { Graphics::StorageTexture::NoteKeysFill(base, bytes, static_cast(pattern[0])); Graphics::StorageTexture::ClearByKeysFill(base, bytes, static_cast(pattern[0])); @@ -247,29 +302,23 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::spanFillBuffer(base, bytes, pattern); + const bool stored = cleared || absorbed || localDevice->FillBuffer(base, bytes, pattern); phase(FillDevice); - if (uniformKeysFill && stored && !cleared) { - Graphics::DccKeys filled = Graphics::DccKeys::Mixed; - switch (pattern[0] & 0xffu) { - case 0x00: filled = Graphics::DccKeys::Clear0000; break; - case 0x40: filled = Graphics::DccKeys::Clear0001; break; - case 0x80: filled = Graphics::DccKeys::Clear1110; break; - case 0xc0: filled = Graphics::DccKeys::Clear1111; break; - case 0x20: filled = Graphics::DccKeys::ClearRegister; break; - case 0xff: filled = Graphics::DccKeys::Uncompressed; break; - default: break; - } - if (filled != Graphics::DccKeys::Mixed) Graphics::NoteKeysFillOnGpu(base, bytes, filled); + if (stored && !cleared && !absorbed) { + if (const auto filled = KeysFillMemo(pattern, uniformKeysFill, memoryFill.has_value())) Graphics::NoteKeysFillOnGpu(base, bytes, *filled); } - if (profile && stored && !cleared) { - const bool uniform = pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]; + if (profile && stored && !cleared && !absorbed) { ++(uniform ? uniformFills : patternFills); (uniform ? uniformBytes : patternBytes) += bytes; if (!uniform) chainCopies += static_cast(std::bit_width((bytes / 16) - 1)); @@ -286,7 +335,7 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(block).first(std::min(chunk, bytes - done)), 16); phase(FillCpu); } - if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) { + if (uniform) { Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); } @@ -304,6 +353,9 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(absorbedFills), static_cast(absorbedBytes) / 1048576.0); + line += absorbedText; line += "; hold by cover (count/ms):"; for (std::size_t i = 0; i < coverNames.size(); ++i) { std::snprintf(text, sizeof(text), " %s %llu/%.0f", coverNames[i], static_cast(holdByCover[i]), holdByCoverMs[i]); 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 c65419b0ee..1ecbf36a71 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -302,6 +302,10 @@ void Driver::execute(const Submission& submission) { const auto condition = Pm4::ReadCondition(packet); if (condition == 0) nextCursor = submission.conditionalEnds.at(cursor); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f queue 0x%x COND_EXEC at DWORD %zu reads 0x%x at 0x%llx: %s %zu dwords\n", TraceMs(), submission.queue, cursor, condition, static_cast(packet[1] | (static_cast(packet[2]) << 32u)), condition == 0 ? "skips" : "executes", submission.conditionalEnds.at(cursor) - cursor - count); + } else if (opcode == 0x91) { + landPendingWaits(submission.queue); + recordQueuedLabelsBeforeRead(submission.queue); + Pm4::Execute(packet, queue); } else if (sampleDump && !wroteOnGpu) { dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { 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 4ad3671f97..a57ea0b88c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -91,6 +91,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t)); try { const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(snapshot.code); + if (const auto tail = ShaderRecompiler::FindUserDataTail(decoded, 0u, 106u)) snapshot.userDataTailRegister = tail->userDataRegister; const auto& last = decoded.instructions.back(); snapshot.code.resize(last.programCounter / sizeof(std::uint32_t) + last.wordCount); auto result = std::make_shared(std::move(snapshot)); @@ -120,6 +121,16 @@ std::shared_ptr ReadRawShader(std::uint64_t address) { throw std::runtime_error("AGC driver: raw shader program has no reachable end within mapped code or the size limit"); } +std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData) { + const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + const auto tail = ShaderRecompiler::FindUserDataTail(program, userDataBase, static_cast(userData.size())); + if (!tail) return nullptr; + const auto slot = tail->userDataRegister - userDataBase; + const auto address = userData[slot] | (static_cast(userData[slot + 1u]) << 32u); + require(address != 0 && (address & 3u) == 0u, "user-data tail continuation address is null or not dword aligned"); + return ReadRawShader(address); +} + std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types) { if (auto registered = RegisteredProgram(shaders, address, types)) return registered; require(shaders.empty(), "graphics program does not belong to a compatible registered shader"); @@ -168,10 +179,11 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset return used; } -std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl) { +std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span code, std::uint32_t paClVsOutCntl, const ShaderSnapshot* continuation) { constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; if (bits == 0) return 0; + if (continuation != nullptr && PositionExportLayout(continuation, 0, continuation->code, bits) != 0) return bits; const bool cached = snapshot != nullptr && codeOffset == 0; if (cached) { const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); @@ -199,7 +211,22 @@ bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& regi const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second) << 40u); const auto snapshot = ProgramSnapshot(registry, address, {2}); require((address - snapshot->codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); - return (snapshot->header.empty() || snapshot->type == 2) && ProgramUsesWorkgroup(*snapshot, (address - snapshot->codeAddress) / sizeof(std::uint32_t)); + const auto offset = (address - snapshot->codeAddress) / sizeof(std::uint32_t); + if (!snapshot->header.empty()) return snapshot->type == 2 && ProgramUsesWorkgroup(*snapshot, offset); + if (!snapshot->userDataTailRegister) return ProgramUsesWorkgroup(*snapshot, offset); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8b); + const auto resources = queue.shader.find(0x8b); + const auto rsrc2 = resources == queue.shader.end() ? 0u : resources->second; + const auto count = ((rsrc2 >> 1u) & 0x1fu) | (((rsrc2 >> 27u) & 1u) << 5u); + require(count <= 32, "graphics user SGPR count exceeds the register bank"); + std::vector userData; + for (std::uint32_t index = 0; index < count; ++index) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0x8c + index); + const auto found = queue.shader.find(0x8c + index); + userData.push_back(found == queue.shader.end() ? 0u : found->second); + } + const auto continuation = ReadUserDataTailContinuation(std::span(snapshot->code).subspan(offset), 8u, userData); + return ProgramUsesWorkgroup(*snapshot, offset) || (continuation != nullptr && ProgramUsesWorkgroup(*continuation, 0u)); } struct ShaderPreparationTransaction::State { @@ -324,6 +351,17 @@ static Shader GuestShaderFields(const Shader* shader) { return fields; } +static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + const auto* bound = used.sh_registers; + used.sh_registers = registered.sh_registers; + if (std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + const auto address = reinterpret_cast(registered.sh_registers); + const std::size_t bytes = registered.num_sh_registers * sizeof(ShaderRegister); + if (bytes == 0 || bound == nullptr || address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size() || snapshot.header.size() - (address - snapshot.headerAddress) < bytes) return false; + GuestMemory::CheckRange(bound, bytes, alignof(ShaderRegister)); + return std::memcmp(static_cast(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0; +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -333,7 +371,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 || FusedHullHeader(snapshot, used, registered); + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); } std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { @@ -458,6 +496,15 @@ ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, con namespace { +std::vector LinkVertexUserDataTail(const ShaderRecompiler::RecompileRequest& request, const ShaderSnapshot& snapshot) { + if (request.shader.stage != ShaderRecompiler::ShaderStage::Vertex || !request.shader.header.empty() || !snapshot.userDataTailRegister) return {}; + const auto program = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(request.shader.code); + const auto tail = ShaderRecompiler::FindUserDataTail(program, request.context.userDataBaseRegister, static_cast(request.context.userData.size())); + if (!tail) return {}; + const auto continuation = ReadUserDataTailContinuation(request.shader.code, request.context.userDataBaseRegister, request.context.userData); + return ShaderRecompiler::LinkUserDataTail(program, *tail, ShaderRecompiler::RdnaInstructionDecoder{}.Decode(continuation->code)); +} + std::string hexText(std::uint64_t value) { char text[24]; std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); @@ -512,7 +559,8 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot"); const auto code = std::span(snapshot.code).subspan(codeOffset); require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code"); - const auto linked = LinkUserDataCallees(request); + auto linked = LinkUserDataCallees(request); + if (linked.empty()) linked = LinkVertexUserDataTail(request, snapshot); auto source = request; if (!linked.empty()) source.shader.code = linked; auto invocationRequest = source; @@ -970,7 +1018,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; std::optional vertex; if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); - if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl) | decoded.state.layerExports; + if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports; pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; 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 9d15cf04bc..3deebcb869 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -140,6 +140,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (heldAtSubmit) return; if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; return; } @@ -332,6 +333,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que while (!Pm4::WaitSatisfiedUnchecked(packet)) { if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); + else epochPoint.bump = false; return; } ++polls; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b284..31152a7138 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -66,6 +66,15 @@ bool readableProtection(DWORD protection) { bool writableProtection(DWORD protection) { return protection == PAGE_READWRITE || protection == PAGE_WRITECOPY || protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY; } + +std::uintptr_t userAddressLimit() { + static const auto limit = [] { + SYSTEM_INFO info{}; + GetSystemInfo(&info); + return reinterpret_cast(info.lpMaximumApplicationAddress); + }(); + return limit; +} #endif } @@ -526,6 +535,10 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { // before it invalidates, so a generation change across the query and the store below means // the stored pages may describe the old state and are forgotten again. const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + if (cursor > userAddressLimit()) { + static_cast(emit(PageRun{cursor, end, false, false})); + return true; + } if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) return false; // Debug aid: APS5_TRACE_QUERY prints every 16th page query with its cost. static const bool traceQuery = std::getenv("APS5_TRACE_QUERY") != nullptr; @@ -749,6 +762,7 @@ struct WriteTracker { // records in the same block (the title's per-job slots are 0x20 apart) must not count. std::vector cpuBlocks; 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; @@ -791,6 +805,7 @@ struct WriteTracker { blocks.assign(size / WriteBlockBytes + 1, 0); cpuBlocks.assign(size / WriteBlockBytes + 1, 0); coverage.Initialize(base, size); + hintBlocks.assign(size / WriteBlockBytes + 1, 0); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); #else @@ -823,6 +838,35 @@ struct WriteTracker { #endif } +#ifdef _WIN32 + void startHints(std::uint64_t address, std::size_t bytes) { + const auto first = std::max(address, base); + const auto stop = std::min(address + bytes, base + size); + 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 (!started) ++generation; + started = true; + hintBlocks[block] = generation; + } + } + + 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; + } + 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]); + return newest; + } +#endif + struct DriverPiece { std::uint32_t generation = 0; std::uint32_t begin = 0; @@ -1173,9 +1217,16 @@ std::uint64_t CollectEpoch() { namespace { +std::atomic unwatchHook{nullptr}; + +void beforeUnwatch(std::uint64_t address, std::size_t bytes) { + if (const auto hook = unwatchHook.load(std::memory_order_acquire)) hook(address, bytes); +} + void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) { if (!tracker.watched) return; #ifdef _WIN32 + tracker.startHints(address, bytes); if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release); #else if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release); @@ -1184,8 +1235,13 @@ void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t byt } +void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) { + unwatchHook.store(hook, std::memory_order_release); +} + void Unwatch(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return; + beforeUnwatch(address, bytes); auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1194,6 +1250,11 @@ void Unwatch(std::uint64_t address, std::size_t bytes) { bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); + if (!staysWatched) { + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + beforeUnwatch(first, static_cast(((address + bytes + page - 1) & ~(page - 1)) - first)); + } auto& tracker = Tracker(); auto lock = lockTracker(tracker); tracker.initialize(); @@ -1212,7 +1273,14 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function saved.assign(source, source + (stop - first)); } const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); - if (!import()) return false; + bool imported = false; + { +#ifdef __APPLE__ + const GuestArena::HostWrite open(reinterpret_cast(first), static_cast(stop - first)); +#endif + imported = import(); + } + if (!imported) return false; if (!staysWatched) { if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); unwatchLocked(tracker, address, bytes); @@ -1263,6 +1331,30 @@ bool UnchangedSinceAll(std::span queries) { return true; } +std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector>& ranges) { + ranges.clear(); + if (bytes == 0 || bytes > std::numeric_limits::max() - address || !WriteWatched()) return 0; + auto committed = CommittedRanges(address, bytes); + auto generation = TrackerGeneration(); + for (const auto& [begin, end] : committed) { + generation = CollectWrites(begin, static_cast(end - begin)); + if (generation == 0) return 0; + } + ranges = std::move(committed); + return generation; +} + +bool UnchangedCommittedSince(std::span> ranges, std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || generation == 0) return false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || !tracker.covers(begin, static_cast(end - begin)) || tracker.newestStamp(begin, end) > generation) return false; + } + return true; +} + std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function()>& store) { if (bytes == 0) return 0; const auto stampStored = [](WriteTracker& tracker, std::pair stored) -> std::uint64_t { @@ -1394,6 +1486,60 @@ std::uint64_t NewestStamp(std::uint64_t address, std::size_t bytes) { return bytes == 0 ? 0 : tracker.newestStamp(address, address + bytes); } +#ifdef _WIN32 +namespace { + +thread_local ThreadCollectMemo threadHintMemo; + +bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) { + auto cursor = first; + bool marked = false; + while (cursor < stop) { + std::size_t count = tracker.pages.size(); + 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; + if (count < tracker.pages.size()) break; + cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; + } + if (marked) collectDirty.fetch_add(1, std::memory_order_relaxed); + return true; +} + +} +#endif + +std::uint64_t UntrackedHint(std::uint64_t address, std::size_t bytes) { +#ifdef _WIN32 + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; + constexpr std::uint64_t page = 4096; + const auto first = address & ~(page - 1); + const auto stop = (address + bytes + page - 1) & ~(page - 1); + const auto epoch = currentCollectEpoch(); + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.hinted(first, stop)) return 0; + const auto unwatched = unwatchSerial.load(std::memory_order_relaxed); + const bool memoized = collectMemoEnabled() && std::any_of(threadHintMemo.entries.begin(), threadHintMemo.entries.end(), [&](const WriteTracker::Memo& entry) { + return entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end; + }); + if (memoized) { + collectMemoHits.fetch_add(1, std::memory_order_relaxed); + } else { + const TimedAccess timed(CounterCollect, bytes); + if (!walkHints(tracker, first, stop)) return 0; + if (collectMemoEnabled()) threadHintMemo.entries[threadHintMemo.next++ % threadHintMemo.entries.size()] = {first, stop, epoch, unwatched}; + } + return tracker.newestHint(first, stop); +#else + static_cast(address); + static_cast(bytes); + return 0; +#endif +} + bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { if (CollectWritesUncached(address, bytes) == 0) return false; auto& tracker = Tracker(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index d2320c3f5c..6d1b13254a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -30,7 +30,16 @@ constexpr std::size_t GpuRangeCapacity = TraceThread::DefaultCapacity; struct Session { std::mutex mutex; std::vector> threads; + bool started = false; std::optional window; + bool windowOpen = false; + std::vector> windowBase; + std::optional interval; + bool intervalOpen = false; + std::uint64_t intervalFirstPresent = 0; + double intervalGpuBusy = 0; + std::vector> intervalBase; + std::vector intervalPresents; Clock::time_point first{}; std::uint64_t present = 0; std::uint64_t firstPresent = 0; @@ -49,6 +58,16 @@ std::unique_ptr& writer() { return output; } +std::unique_ptr& summaryWriter() { + 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 : "."); +} + std::uint64_t processId() { #ifdef _WIN32 return GetCurrentProcessId(); @@ -91,29 +110,109 @@ std::vector> environment() { return result; } +TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + TraceSummary summary; + summary.commit = APS5_TRACE_COMMIT; + if (APS5_TRACE_DIRTY >= 0) summary.dirty = APS5_TRACE_DIRTY != 0; + summary.environment = environment(); + summary.gpu = device != nullptr ? device->DeviceName() : std::string(); + summary.driver = device != nullptr ? device->DriverInfo() : std::string(); + summary.os = operatingSystem(); + summary.width = width; + summary.height = height; + return summary; +} + +std::vector> totalsOf(const std::vector>& threads) { + std::vector> result; + for (const auto& thread : threads) result.push_back(thread->Collect().totals); + return result; +} + +double seconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); +} + } void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { if (!Configured() || local != nullptr) return; - auto bound = std::make_unique(name, presentCount, capacity); + const bool windowed = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + auto bound = std::make_unique(name, presentCount, windowed ? capacity : 0, buffering); auto& state = session(); std::lock_guard lock(state.mutex); local = bound.get(); state.threads.push_back(std::move(bound)); } +void PerformanceTrace::OpenInterval(Clock::time_point now, std::uint64_t present) { + auto& state = session(); + state.interval->Restart(now - state.first); + state.intervalOpen = true; + state.intervalFirstPresent = present; + state.intervalGpuBusy = VulkanDevice::PresentCounts().gpuBusyMs; + state.intervalPresents.assign(1, now); + if (summaryWriter()) return; + summaryWriter() = std::make_unique([directory = traceDirectory()](std::string_view text) { + try { + std::filesystem::create_directories(directory); + TraceFile::AppendLine(directory / "aps5-summary.jsonl", text); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] appending to aps5-summary.jsonl failed: %s\n", error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] summarizing %s from present %llu\n", state.interval->Describe().c_str(), static_cast(present)); +} + +void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { + auto& state = session(); + state.intervalBase.resize(state.threads.size()); + std::vector snapshots; + for (std::size_t index = 0; index < state.threads.size(); ++index) { + auto snapshot = state.threads[index]->Collect(); + auto totals = std::move(snapshot.totals); + snapshot.slices = {}; + snapshot.totals = TraceFormat::Since(totals, state.intervalBase[index]); + state.intervalBase[index] = std::move(totals); + snapshots.push_back(std::move(snapshot)); + } + auto summary = identity(width, height, device); + summary.window = state.interval->Describe(); + summary.firstPresent = state.intervalFirstPresent; + 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)); +} + void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) { const auto now = Clock::now(); BindThread("presenter"); auto& state = session(); std::lock_guard lock(state.mutex); - if (!state.window) { - state.window.emplace(TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS")).value()); + 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.first = now; + state.started = true; } const auto present = state.present++; - const auto edge = state.window->Present(present, now - state.first); + const auto edge = state.window ? state.window->Present(present, now - state.first) : TraceWindow::Edge::None; + if (state.interval) { + if (!state.intervalOpen) { + OpenInterval(now, present); + } else { + state.intervalPresents.push_back(now); + if (state.interval->Due(now - state.first)) { + FlushInterval(now, width, height, device); + OpenInterval(now, present); + } + } + } if (edge == TraceWindow::Edge::Open) { + state.windowOpen = true; + state.windowBase = totalsOf(state.threads); state.firstPresent = present; state.gpuBusyAtOpen = VulkanDevice::PresentCounts().gpuBusyMs; TraceClockSample clock; @@ -125,21 +224,31 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const } state.presents.push_back(now); presentCount.fetch_add(1, std::memory_order_relaxed); + buffering.store(true, std::memory_order_relaxed); recording.store(true, std::memory_order_relaxed); ProfilePrint_nid_no_patch("[trace] recording %s from present %llu%s\n", state.window->Describe().c_str(), static_cast(present), state.gpuClock ? "" : "; no GPU track: the device has no calibrated timestamps"); return; } - if (!recording.load(std::memory_order_relaxed)) return; + if (!state.windowOpen) { + if (!state.interval) return; + presentCount.fetch_add(1, std::memory_order_relaxed); + recording.store(true, std::memory_order_relaxed); + return; + } state.presents.push_back(now); presentCount.fetch_add(1, std::memory_order_relaxed); if (edge != TraceWindow::Edge::Close) return; - recording.store(false, std::memory_order_relaxed); + state.windowOpen = false; + buffering.store(false, std::memory_order_relaxed); + recording.store(state.interval.has_value(), std::memory_order_relaxed); std::vector snapshots; std::size_t presenter = 0; + state.windowBase.resize(state.threads.size()); for (std::size_t index = 0; index < state.threads.size(); ++index) { if (state.threads[index].get() == local) presenter = index; snapshots.push_back(state.threads[index]->Collect()); + snapshots.back().totals = TraceFormat::Since(snapshots.back().totals, state.windowBase[index]); } std::vector gpuSlices; TraceClockSample clock; @@ -149,23 +258,14 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); } const auto process = processId(); - TraceSummary summary; - summary.commit = APS5_TRACE_COMMIT; - if (APS5_TRACE_DIRTY >= 0) summary.dirty = APS5_TRACE_DIRTY != 0; - summary.environment = environment(); - summary.gpu = device != nullptr ? device->DeviceName() : std::string(); - summary.driver = device != nullptr ? device->DriverInfo() : std::string(); - summary.os = operatingSystem(); - summary.width = width; - summary.height = height; + auto summary = identity(width, height, device); summary.window = state.window->Describe(); summary.firstPresent = state.firstPresent; summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.gpuBusyAtOpen; summary.trace = "aps5-trace-" + summary.commit + "-" + std::to_string(process) + ".json.gz"; auto events = TraceFormat::Events(process, state.presents.front(), snapshots, presenter, state.presents, state.firstPresent); - const char* traceDirectory = std::getenv("APS5_TRACE_DIR"); auto& output = writer(); - output = std::make_unique([directory = std::filesystem::path(traceDirectory != nullptr ? traceDirectory : "."), name = summary.trace, line = TraceFormat::Summary(summary, snapshots, state.presents)](std::string_view text) { + output = std::make_unique([directory = traceDirectory(), name = summary.trace, line = TraceFormat::Summary(summary, snapshots, state.presents)](std::string_view text) { try { std::filesystem::create_directories(directory); TraceFile::WriteGzip(directory / name, text); @@ -184,6 +284,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const void PerformanceTrace::StopOutput() { if (writer()) writer()->Stop(); + if (summaryWriter()) summaryWriter()->Stop(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 2fdd6b930b..3563c24a97 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -198,7 +198,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27: case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x45: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: - case 0x81: case 0x83: case 0x9f: case 0x28: return {}; + case 0x81: case 0x83: case 0x91: case 0x9f: case 0x28: return {}; case 0x24: case 0x25: case 0x2c: case 0x38: if (IndirectDrawsDisabled()) return "graphics draw, shader stages and guest render-target materialization are not implemented"; return {}; @@ -301,6 +301,13 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[5] & ~0x20u) == 0, "unsupported indexed draw flags"); break; case 0x2a: graphics(); size(2); require(packet[1] <= 3, "unsupported index-type modifiers"); break; + case 0x91: + graphics(); + size(4); + require((packet[1] & 1u) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode (addr_offset) is not implemented"); + require((packet[1] & 0xeu) == 0, "INDEX_ATTRIBUTES_INDIRECT attribute base is not 16-byte aligned"); + require((packet[3] >> 16u) == 0, "INDEX_ATTRIBUTES_INDIRECT reserved attribute index bits are set"); + break; case 0x2d: graphics(); size(3); @@ -647,7 +654,7 @@ bool UsesGpuCacheBarrier(std::span packet) { bool AccessesMemory(std::uint32_t header) { switch ((header >> 8u) & 0xffu) { - case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; + case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x91: case 0x9f: return true; default: return false; } } @@ -828,6 +835,40 @@ void ExecuteIndirectRegisters(std::span packet, std::span words; +}; + +IndexAttributes readIndexAttributes(std::span packet) { + require(packet.size() == 4 && ((packet[0] >> 8u) & 0xffu) == 0x91, "expected INDEX_ATTRIBUTES_INDIRECT packet"); + require((packet[1] & 0xfu) == 0, "INDEX_ATTRIBUTES_INDIRECT indirect mode or reserved base bits are not implemented"); + const auto base = address(packet[1], packet[2]); + const auto offset = static_cast(packet[3] & 0xffffu) * IndexAttributesBytes; + require(offset <= std::numeric_limits::max() - base, "INDEX_ATTRIBUTES_INDIRECT record address overflow"); + IndexAttributes attributes{base + offset, {}}; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::IndirectArguments); + GuestMemory::Read(attributes.address, std::as_writable_bytes(std::span(attributes.words)), IndexAttributesBytes); + return attributes; +} + +void executeIndexAttributes(const IndexAttributes& attributes, QueueState& queue) { + if (attributes.words[3] > 3) { + char what[160]; + std::snprintf(what, sizeof(what), "INDEX_ATTRIBUTES_INDIRECT record at 0x%llx has index type 0x%x (index-type modifiers are not implemented)", static_cast(attributes.address), attributes.words[3]); + throw std::runtime_error(what); + } + queue.indexBase = address(attributes.words[0], attributes.words[1]); + queue.indexBufferSize = attributes.words[2]; + queue.indexType = attributes.words[3]; +} + +} + void Execute(std::span packet, QueueState& queue) { const auto opcode = (packet[0] >> 8u) & 0xffu; switch (opcode) { @@ -874,6 +915,7 @@ void Execute(std::span packet, QueueState& queue) { case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0]))); case 0x26: queue.indexBase = address(packet[1], packet[2]); return; case 0x2a: queue.indexType = packet[1]; return; + case 0x91: executeIndexAttributes(readIndexAttributes(packet), queue); return; case 0x2f: queue.instanceCount = packet[1]; return; case 0x63: case 0x64: case 0x9f: ExecuteIndirectRegisters(packet, ReadIndirectRegisters(packet), queue); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp new file mode 100644 index 0000000000..ddb45a05f2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -0,0 +1,368 @@ +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +constexpr std::uint32_t ModeTenBit = 1; +constexpr std::uint32_t ModeRedLow = 2; +constexpr std::uint32_t ModeKeepTenBit = 4; +constexpr std::uint32_t ModeSubsample = 8; +constexpr std::uint32_t ModePq = 16; + +} + +PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "presentation pass needs at least one slot"); + try { + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Graphics::Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler presentation pass"); + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, &sampler}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + descriptorInfo.bindingCount = static_cast(bindings.size()); + descriptorInfo.pBindings = bindings.data(); + Graphics::Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout presentation pass"); + const VkPushConstantRange push{VK_SHADER_STAGE_FRAGMENT_BIT, 0, 36}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &descriptorLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout presentation pass"); + const std::array sizes{{{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, static_cast(slotCount)}, {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(slotCount)}}}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = static_cast(slotCount); + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); + Graphics::Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool presentation pass"); + for (auto& slot : slots) { + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = descriptorPool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &descriptorLayout; + Graphics::Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &slot.set), "vkAllocateDescriptorSets presentation pass"); + } + const auto& luminance = PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const VkDescriptorBufferInfo tableInfo{pqNits->Handle(), 0, sizeof(luminance)}; + std::vector tableWrites(slots.size(), VkWriteDescriptorSet{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}); + for (std::size_t i = 0; i < slots.size(); ++i) { + tableWrites[i].dstSet = slots[i].set; + tableWrites[i].dstBinding = 1; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + 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"); + } catch (...) { + release(); + throw; + } +} + +PresentationPass::~PresentationPass() { + release(); +} + +void PresentationPass::release() noexcept { + for (std::size_t i = 0; i < slots.size(); ++i) { + Release(i); + releaseDumpTarget(slots[i]); + } + for (const auto& [key, entry] : targets) { + if (entry.pipeline) context.Function("vkDestroyPipeline")(context.device, entry.pipeline, nullptr); + if (entry.renderPass) context.Function("vkDestroyRenderPass")(context.device, entry.renderPass, nullptr); + } + targets.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (descriptorPool) context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); + if (sampler) context.Function("vkDestroySampler")(context.device, sampler, nullptr); + pqNits.reset(); + fragment = vertex = VK_NULL_HANDLE; + descriptorPool = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + descriptorLayout = VK_NULL_HANDLE; + sampler = VK_NULL_HANDLE; +} + +bool PresentationPass::TenBitFormat(VkFormat format) { + return format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || format == VK_FORMAT_A2R10G10B10_UNORM_PACK32; +} + +VkExtent2D PresentationPass::DumpExtent(std::uint32_t width, std::uint32_t height, std::uint32_t stride) { + Graphics::Require(stride != 0, "presentation dump stride must be non-zero"); + return {(width + stride - 1) / stride, (height + stride - 1) / stride}; +} + +void PresentationPass::Release(std::size_t index) noexcept { + if (index >= slots.size()) return; + auto& slot = slots[index]; + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); + if (slot.source) context.Function("vkDestroyImageView")(context.device, slot.source, nullptr); + slot.source = VK_NULL_HANDLE; +} + +VkImageView PresentationPass::createView(VkImage image, VkFormat format, VkImageUsageFlags usage, std::uint32_t layer) const { + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = usage; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.pNext = &usageInfo; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, layer, 1}; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView presentation pass"); + return view; +} + +VkFramebuffer PresentationPass::createFramebuffer(VkRenderPass renderPass, VkImageView view, VkExtent2D extent) const { + VkFramebufferCreateInfo info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + info.renderPass = renderPass; + info.attachmentCount = 1; + info.pAttachments = &view; + info.width = extent.width; + info.height = extent.height; + info.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateFramebuffer")(context.device, &info, nullptr, &framebuffer), "vkCreateFramebuffer presentation pass"); + return framebuffer; +} + +const PresentationPass::Target& PresentationPass::target(VkFormat format, VkImageLayout finalLayout) { + const auto key = std::pair{format, finalLayout}; + if (const auto found = targets.find(key); found != targets.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "presentation pass format is not renderable"); + Target created; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &attachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 2; + renderPassInfo.pDependencies = dependencies; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &renderPassInfo, nullptr, &created.renderPass), "vkCreateRenderPass presentation pass"); + + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{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 viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewport; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.pDynamicState = &dynamic; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = created.renderPass; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &created.pipeline), "vkCreateGraphicsPipelines presentation pass"); + } catch (...) { + if (created.renderPass) context.Function("vkDestroyRenderPass")(context.device, created.renderPass, nullptr); + throw; + } + return targets.emplace(key, created).first->second; +} + +void PresentationPass::Bind(std::size_t index, VkImage image, std::uint32_t width, std::uint32_t height, std::uint64_t pixelFormat, float whiteNits, std::uint32_t layer) { + Graphics::Require(index < slots.size(), "presentation pass slot is out of range"); + Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation pass source is unavailable"); + Graphics::Require(!DisplayHalfFloat(pixelFormat), "presentation pass does not decode 16-bit float display buffers"); + Graphics::Require(std::isfinite(whiteNits) && whiteNits >= 1.0f, "presentation pass white level must be at least 1 nit"); + Release(index); + auto& slot = slots[index]; + slot.source = createView(image, VK_FORMAT_R32_UINT, VK_IMAGE_USAGE_SAMPLED_BIT, layer); + slot.width = width; + slot.height = height; + slot.mode = (DisplayTenBit(pixelFormat) ? ModeTenBit : 0u) | (DisplayRedLow(pixelFormat) ? ModeRedLow : 0u) | (DisplayPq(pixelFormat) ? ModePq : 0u); + slot.whiteNits = whiteNits; + const VkDescriptorImageInfo imageInfo{sampler, slot.source, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = slot.set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); +} + +void PresentationPass::draw(VkCommandBuffer commands, const Slot& slot, const Target& entry, VkFramebuffer framebuffer, VkExtent2D extent, const std::int32_t (¶meters)[9]) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = entry.renderPass; + 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, entry.pipeline); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &slot.set, 0, nullptr); + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), parameters); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + context.Function("vkCmdEndRenderPass")(commands); +} + +void PresentationPass::RecordPresent(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkExtent2D extent, VkImageLayout finalLayout) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "presentation pass destination is unavailable"); + auto& slot = slots[index]; + const auto& entry = target(format, finalLayout); + const auto view = createView(destination, format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u)); + VkFramebuffer framebuffer = VK_NULL_HANDLE; + try { + framebuffer = createFramebuffer(entry.renderPass, view, extent); + } catch (...) { + context.Function("vkDestroyImageView")(context.device, view, nullptr); + throw; + } + slot.transient.emplace_back(view, framebuffer); + const auto rect = ComputeContainRect_nid_postfix(slot.width, slot.height, extent.width, extent.height); + const std::uint32_t mode = slot.mode | (TenBitFormat(format) ? ModeKeepTenBit : 0u); + const std::int32_t parameters[9] = {rect.x, rect.y, static_cast(rect.width), static_cast(rect.height), static_cast(slot.width), static_cast(slot.height), 1, static_cast(mode), std::bit_cast(slot.whiteNits)}; + draw(commands, slot, entry, framebuffer, extent, parameters); +} + +void PresentationPass::releaseDumpTarget(Slot& slot) noexcept { + if (slot.dumpFramebuffer) context.Function("vkDestroyFramebuffer")(context.device, slot.dumpFramebuffer, nullptr); + if (slot.dumpView) context.Function("vkDestroyImageView")(context.device, slot.dumpView, nullptr); + if (slot.dumpImage) context.Function("vkDestroyImage")(context.device, slot.dumpImage, nullptr); + if (slot.dumpMemory) context.Function("vkFreeMemory")(context.device, slot.dumpMemory, nullptr); + slot.dumpFramebuffer = VK_NULL_HANDLE; + slot.dumpView = VK_NULL_HANDLE; + slot.dumpImage = VK_NULL_HANDLE; + slot.dumpMemory = VK_NULL_HANDLE; + slot.dumpExtent = {}; +} + +void PresentationPass::ensureDumpTarget(Slot& slot, VkExtent2D extent) { + if (slot.dumpImage != VK_NULL_HANDLE && slot.dumpExtent.width == extent.width && slot.dumpExtent.height == extent.height) return; + releaseDumpTarget(slot); + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {extent.width, extent.height, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &slot.dumpImage), "vkCreateImage presentation dump"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, slot.dumpImage, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &slot.dumpMemory), "vkAllocateMemory presentation dump"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, slot.dumpImage, slot.dumpMemory, 0), "vkBindImageMemory presentation dump"); + slot.dumpView = createView(slot.dumpImage, VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + slot.dumpFramebuffer = createFramebuffer(target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL).renderPass, slot.dumpView, extent); + slot.dumpExtent = extent; + } catch (...) { + releaseDumpTarget(slot); + throw; + } +} + +void PresentationPass::RecordDump(VkCommandBuffer commands, std::size_t index, std::uint32_t stride, VkBuffer destination) { + Graphics::Require(index < slots.size() && slots[index].source != VK_NULL_HANDLE, "presentation pass slot has no source"); + Graphics::Require(destination != VK_NULL_HANDLE, "presentation dump buffer is unavailable"); + auto& slot = slots[index]; + const auto extent = DumpExtent(slot.width, slot.height, stride); + ensureDumpTarget(slot, extent); + const auto& entry = target(VK_FORMAT_B8G8R8A8_UNORM, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + const std::int32_t parameters[9] = {0, 0, static_cast(extent.width), static_cast(extent.height), static_cast(slot.width), static_cast(slot.height), static_cast(stride), static_cast(slot.mode | ModeSubsample), std::bit_cast(slot.whiteNits)}; + draw(commands, slot, entry, slot.dumpFramebuffer, extent, parameters); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {extent.width, extent.height, 1u}; + context.Function("vkCmdCopyImageToBuffer")(commands, slot.dumpImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination, 1, ©); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index 354b2f0cbd..d7c88d7b7d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -17,6 +17,8 @@ PresentationScaler::PresentationScaler(const Graphics::Context& context, VkForma PresentationScaler::~PresentationScaler() { release(); + if (blackImage != VK_NULL_HANDLE) context.Function("vkDestroyImage")(context.device, blackImage, nullptr); + if (blackMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, blackMemory, nullptr); } void PresentationScaler::release() noexcept { @@ -102,7 +104,7 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); } -void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height) { +void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter, std::uint32_t width, std::uint32_t height, std::uint32_t layer) { Graphics::Require(sourceImage != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "presentation image is unavailable"); Graphics::Require(width != 0 && height != 0, "presentation blit extent must be non-zero"); const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); @@ -116,9 +118,9 @@ void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; - blit.dstSubresource = blit.srcSubresource; + blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; blit.dstOffsets[1] = {static_cast(sourceWidth), static_cast(sourceHeight), 1}; context.Function("vkCmdBlitImage")(commands, image, layout, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -142,11 +144,11 @@ void PresentationScaler::RecordBlit(VkCommandBuffer commands, VkImage destinatio RecordBlitFrom(context, commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sourceWidth, sourceHeight, VK_FILTER_LINEAR, destinationImage, destinationWidth, destinationHeight); } -void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { +void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, VkFilter filter, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight, std::uint32_t layer) { Graphics::Require(image != VK_NULL_HANDLE && width != 0 && height != 0, "presentation blit source is unavailable"); const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); VkImageBlit blit{}; - blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; blit.srcOffsets[0] = {0, 0, 0}; blit.srcOffsets[1] = {static_cast(width), static_cast(height), 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; @@ -155,4 +157,82 @@ void PresentationScaler::RecordBlitFrom(const Graphics::Context& context, VkComm context.Function("vkCmdBlitImage")(commands, image, layout, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, filter); } +std::vector PresentationScaler::LetterboxBands(std::uint32_t width, std::uint32_t height, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto rect = ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto right = rect.x + static_cast(rect.width); + const auto bottom = rect.y + static_cast(rect.height); + std::vector bands; + if (rect.y > 0) bands.push_back({0, 0, destinationWidth, static_cast(rect.y)}); + if (bottom < static_cast(destinationHeight)) bands.push_back({0, bottom, destinationWidth, destinationHeight - static_cast(bottom)}); + if (rect.x > 0) bands.push_back({0, rect.y, static_cast(rect.x), rect.height}); + if (right < static_cast(destinationWidth)) bands.push_back({right, rect.y, destinationWidth - static_cast(right), rect.height}); + return bands; +} + +void PresentationScaler::RecordLetterbox(VkCommandBuffer commands, std::uint32_t width, std::uint32_t height, VkImage destinationImage, std::uint32_t destinationWidth, std::uint32_t destinationHeight) { + const auto bands = LetterboxBands(width, height, destinationWidth, destinationHeight); + if (bands.empty()) return; + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + if (blackImage == VK_NULL_HANDLE) { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = VK_FORMAT_B8G8R8A8_UNORM; + imageInfo.extent = {1u, 1u, 1u}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage letterbox"); + try { + 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); + Graphics::Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory letterbox"); + Graphics::Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory letterbox"); + } catch (...) { + context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, memory, nullptr); + throw; + } + blackImage = image; + blackMemory = memory; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = blackImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + context.Function("vkCmdClearColorImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &barrier.subresourceRange); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + std::vector regions; + regions.reserve(bands.size()); + for (const auto& band : bands) { + VkImageBlit blit{}; + blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + blit.srcOffsets[1] = {1, 1, 1}; + blit.dstSubresource = blit.srcSubresource; + blit.dstOffsets[0] = {band.x, band.y, 0}; + blit.dstOffsets[1] = {band.x + static_cast(band.width), band.y + static_cast(band.height), 1}; + regions.push_back(blit); + } + context.Function("vkCmdBlitImage")(commands, blackImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destinationImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5d3ddfec4c..6e644f6c84 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -14,11 +14,13 @@ #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/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.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/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" @@ -94,7 +96,7 @@ bool CoreVulkanCapability(std::uint32_t capability) { } } -void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) { +void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline, Graphics::PipelineCacheSynchronization* synchronization = nullptr) { VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; info.flags = flags; info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; @@ -107,6 +109,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, subgroup.requiredSubgroupSize = subgroupSize; if (subgroupSize != 0) info.stage.pNext = &subgroup; info.layout = layout; + auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); } @@ -302,6 +305,11 @@ struct VulkanDevice::State { bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; bool dmaBufImport = false; + bool sparseBinding = false; + std::uint32_t queueFamily = 0; + std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + VkQueue sparseQueue = VK_NULL_HANDLE; + std::mutex sparseQueueMutex; bool memoryBudget = false; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -357,6 +365,8 @@ struct VulkanDevice::State { std::shared_ptr>> copiedWriters = std::make_shared>>(); VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; + std::unique_ptr presentation; + VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; // The swapchain image AcquireImage took for the next present(), consumed by that present. bool imageAcquired = false; std::uint32_t acquiredIndex = 0; @@ -540,6 +550,7 @@ struct VulkanDevice::State { // and its statistics taken (under presentMutex). void RetireSlot(PresentSlot& slot) { slot.inFlight = false; + if (presentation) presentation->Release(static_cast(&slot - presentSlots.data())); slot.kept.reset(); if (slot.dumpRecorded) { slot.dumpRecorded = false; @@ -693,6 +704,7 @@ struct VulkanDevice::State { textureCache.reset(); detiler.reset(); colorTransfer.reset(); + presentation.reset(); scaler.reset(); pipelineCache.reset(); context.bufferPool.reset(); @@ -1377,6 +1389,34 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; + VkDeviceQueueCreateInfo queueInfos[2]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; + if (available.sparseBinding && state->dmaBufImport) { + std::uint32_t families = 0; + auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); + getFamilies(selected, &families, nullptr); + std::vector queues(families); + getFamilies(selected, &families, queues.data()); + const auto binds = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; }; + std::uint32_t sparseFamily = families; + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i) && queues[i].queueFlags == VK_QUEUE_SPARSE_BINDING_BIT) sparseFamily = i; + } + for (std::uint32_t i = 0; i < families && sparseFamily == families; ++i) { + if (binds(i)) sparseFamily = i; + } + if (sparseFamily != families) { + queueInfos[1].queueFamilyIndex = sparseFamily; + queueInfos[1].queueCount = 1; + queueInfos[1].pQueuePriorities = &priority; + deviceInfo.queueCreateInfoCount = 2; + state->sparseFamily = sparseFamily; + state->sparseBinding = true; + } else { + state->sparseBinding = family < families && (queues[family].queueFlags & VK_QUEUE_SPARSE_BINDING_BIT) != 0; + } + enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; + } + deviceInfo.pQueueCreateInfos = queueInfos; // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1631,6 +1671,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); + state->queueFamily = family; + if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast(state->device), reinterpret_cast(state->queue), family); VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; @@ -1694,14 +1736,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(getPresentModes(selected, state->surface, &presentModeCount, presentModes.data()), "vkGetPhysicalDeviceSurfacePresentModesKHR"); presentModes.resize(presentModeCount); state->presentMode = SelectPresentMode(std::getenv("ANYPS5_PRESENT_MODE"), presentModes); + require((surface.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0, "surface does not support color attachment images"); VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; swapchain.surface = state->surface; swapchain.minImageCount = surface.minImageCount; - swapchain.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; + swapchain.imageFormat = state->surfaceFormat; swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; swapchain.imageExtent = state->extent; swapchain.imageArrayLayers = 1; - swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -1728,7 +1771,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(createFence(state->device, &fence, nullptr, &slot.fence), "vkCreateFence present"); check(state->DeviceFunction("vkAllocateCommandBuffers")(state->device, &allocation, &slot.commands), "vkAllocateCommandBuffers"); } - state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); + state->presentation = std::make_unique(graphicsContext(), state->presentSlots.size()); } } @@ -2374,11 +2418,11 @@ void VulkanDevice::Resize(std::uint32_t width, std::uint32_t height) { VkSwapchainCreateInfoKHR create{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; create.surface = state->surface; create.minImageCount = surface.minImageCount; - create.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; + create.imageFormat = state->surfaceFormat; create.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; create.imageExtent = {width, height}; create.imageArrayLayers = 1; - create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -2484,10 +2528,12 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } } -ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter) { +ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { const auto& descriptor = image.Descriptor(); - if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageLayers() != 1 || image.ImageDepth() != 1) return ResidentPresent::None; - if (image.GuestBytes() != DisplayBufferSize(buffer)) return ResidentPresent::None; + if (descriptor.width != buffer.width || descriptor.height != buffer.height || descriptor.mipCount != 1 || image.ImageDepth() != 1) return ResidentPresent::None; + const auto slice = ResidentPresentSlice(descriptor.baseAddress, image.GuestBytes(), image.ImageLayers(), buffer.address, DisplayBufferSize(buffer)); + if (!slice) return ResidentPresent::None; + layer = *slice; const auto format = Graphics::StorageFormatForGuest(context, descriptor.format); VkFormatProperties properties{}; context.formatProperties(context.physical, format, &properties); @@ -2496,12 +2542,13 @@ ResidentPresent ResidentPresentable(const Graphics::Context& context, const Grap return ResidentPresentPath(format, buffer.pixelFormat, blitSource); } -std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert) { - auto resident = Graphics::StorageTexture::FindPending(buffer.address, DisplayBufferSize(buffer)); +std::shared_ptr PresentableResident(const Graphics::Context& context, const DisplayBuffer& buffer, VkFilter& filter, bool& pending, bool& convert, std::uint32_t& layer) { + auto resident = Graphics::StorageTexture::FindPendingSlice(buffer.address, DisplayBufferSize(buffer)); pending = resident != nullptr; convert = false; + layer = 0; if (resident == nullptr) return resident; - const auto path = ResidentPresentable(context, *resident, buffer, filter); + const auto path = ResidentPresentable(context, *resident, buffer, filter, layer); if (path == ResidentPresent::None) resident.reset(); convert = path == ResidentPresent::Convert; return resident; @@ -2569,9 +2616,23 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { static const bool storeAtFlip = Graphics::StorageTexture::StoreAtFlipRequested(std::getenv("APS5_STORE_AT_FLIP")); if (storeAtFlip) Graphics::StorageTexture::FlushAllPending("flip"); else Graphics::StorageTexture::FlushDumpSelected("flip"); + auto& dumps = Dumps(); if (buffer.tilingMode == 1) { const auto pixels = ReadDisplayBuffer(buffer); - return present(buffer.width, buffer.height, true, pixels); + bool dumpFrame = false; + if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (dumps.cpu) { + WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, pixels, DumpScale()); + } else { + state->nextDumpIndex = dumps.dumped++; + dumpFrame = true; + } + } + if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame)) { + if (dumpFrame) --dumps.dumped; + return false; + } + return true; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; // The display buffer is usually a resident render target whose results are still on the GPU: it @@ -2584,9 +2645,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::shared_ptr resident; VkFilter filter = VK_FILTER_LINEAR; bool convert = false; + std::uint32_t layer = 0; if (!NoResidentPresent()) { bool pending = false; - resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); + resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert, layer); if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) { resident.reset(); convert = false; @@ -2610,7 +2672,6 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; - auto& dumps = Dumps(); bool dumpFrame = false; if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { @@ -2633,7 +2694,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; uniform = {{channel(2), channel(1), channel(0), channel(3)}}; } - if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr)) { + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; return false; @@ -2682,7 +2743,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform, std::uint32_t residentLayer) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -2700,12 +2761,13 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; + const bool drawn = resident != nullptr && residentConvert && !DisplayHalfFloat(display->pixelFormat); // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them // wait for every slot first. The game path did so before taking the mutex // (PresentWaitsForSlots); this is the fallback. - const bool shared = !clearOnly && !direct; - if ((shared || (!clearOnly && (state->scaler == nullptr || state->scaler->SourceWidth() != width || state->scaler->SourceHeight() != height))) && state->SharedSlotInFlight()) RetirePresents(0); + const bool shared = !clearOnly && !direct && !drawn; + if (shared && state->SharedSlotInFlight()) RetirePresents(0); // The next slot is the oldest; a caller that did not retire it first (PresentPixels) waits here. if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; @@ -2754,8 +2816,10 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu barrier.image = state->images[index]; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); - pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (!drawn) { + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (clearOnly) { APS5_LOG_OUT_DEBUG("Recording swapchain clear opaque=%u image=%p", static_cast(opaque), reinterpret_cast(barrier.image)); VkClearColorValue clear{}; @@ -2763,84 +2827,86 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange); } else { APS5_LOG_OUT_DEBUG("Recording swapchain scaled blit width=%u height=%u bytes=%zu buffer=%p image=%p", width, height, pixels.size(), reinterpret_cast(state->uploadBuffer), reinterpret_cast(barrier.image)); - require(state->scaler != nullptr, "presentation scaler is unavailable"); - state->scaler->EnsureSourceImage(width, height); + require(state->scaler != nullptr && state->presentation != nullptr, "presentation scaler is unavailable"); + if (shared) state->scaler->EnsureSourceImage(width, height); + const auto slotIndex = static_cast(&slot - state->presentSlots.data()); VkImageMemoryBarrier residentBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + const VkPipelineStageFlags residentStage = drawn ? VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT; + const VkAccessFlags residentRead = drawn ? VK_ACCESS_SHADER_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT; if (resident != nullptr) { // The draws and dispatches that produced the image were submitted before this command // buffer; their writes become visible to the blit here. The layout stays GENERAL, the // one the storage image is tracked in. residentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + residentBarrier.dstAccessMask = residentRead; residentBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; residentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; residentBarrier.image = resident->Image(); residentBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS}; - pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + pipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, residentStage, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (residentConvert) { - state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat)); + if (residentConvert && !drawn) { + state->colorTransfer->DetileImage(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, Graphics::ColorTileMode::RenderTarget, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat), residentLayer); state->scaler->RecordUpload(commands, state->colorTransfer->LinearBuffer()); } } else { if (display != nullptr) { - state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat), DisplayHalfFloat(display->pixelFormat)); + const bool pq = DisplayPq(display->pixelFormat); + state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat) && !pq, DisplayHalfFloat(display->pixelFormat), pq ? std::optional(SdrToneMapLuminanceNits) : std::nullopt); } if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } - VkClearColorValue letterbox{}; - letterbox.float32[3] = 1.0f; - state->DeviceFunction("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &letterbox, 1, &barrier.subresourceRange); - VkImageMemoryBarrier letterboxBarrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - letterboxBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - letterboxBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - letterboxBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - letterboxBarrier.image = barrier.image; - letterboxBarrier.subresourceRange = barrier.subresourceRange; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &letterboxBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height); - else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); - if (dumpFrame && direct) { - const auto scale = DumpScale(); - slot.dumpWidth = (width + scale - 1) / scale; - slot.dumpHeight = (height + scale - 1) / scale; - slot.dumpScale = 1; - if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); - slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); - slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height); - } - if (resident != nullptr) { - // Later batches write the image again (the next frame into this buffer, a refresh): they - // start after the blit has read it. - residentBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; - residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (drawn) { + state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, SdrToneMapLuminanceNits, residentLayer); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else { + state->scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); + if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); + else state->scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height); } if (dumpFrame) { - if (!direct) { + const auto scale = DumpScale(); + if (drawn || direct) { + const auto extent = PresentationPass::DumpExtent(width, height, scale); + slot.dumpWidth = extent.width; + slot.dumpHeight = extent.height; + slot.dumpScale = 1; + } else { slot.dumpWidth = state->scaler->SourceWidth(); slot.dumpHeight = state->scaler->SourceHeight(); - slot.dumpScale = DumpScale(); + slot.dumpScale = scale; } const auto dumpBytes = static_cast(slot.dumpWidth) * slot.dumpHeight * 4; if (!slot.dumpBuffer || slot.dumpBuffer->Bytes().size() != dumpBytes) slot.dumpBuffer = std::make_unique(graphicsContext(), dumpBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); - (direct ? slot.dumpScaler : state->scaler)->RecordReadback(commands, slot.dumpBuffer->Handle()); + if (drawn) { + state->presentation->RecordDump(commands, slotIndex, scale, slot.dumpBuffer->Handle()); + } else if (direct) { + if (!slot.dumpScaler) slot.dumpScaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + slot.dumpScaler->EnsureSourceImage(slot.dumpWidth, slot.dumpHeight); + slot.dumpScaler->RecordBlitInto(commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_FILTER_NEAREST, width, height, residentLayer); + slot.dumpScaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } else { + state->scaler->RecordReadback(commands, slot.dumpBuffer->Handle()); + } + } + if (resident != nullptr) { + residentBarrier.srcAccessMask = residentRead; + residentBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + pipelineBarrier(commands, residentStage | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &residentBarrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = 0; - barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; - barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + if (!drawn) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = 0; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); + } if (slot.queries != VK_NULL_HANDLE) state->DeviceFunction("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, slot.queries, 1); check(state->DeviceFunction("vkEndCommandBuffer")(commands), "vkEndCommandBuffer"); APS5_LOG_CHARS_OUT_DEBUG("Presentation command buffer recorded"); @@ -2915,8 +2981,8 @@ bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { VkFilter filter = VK_FILTER_LINEAR; bool pending = false; bool convert = false; - if (PresentableResident(graphicsContext(), *buffer, filter, pending, convert) == nullptr || convert) return true; - return state->scaler == nullptr || state->scaler->SourceWidth() != buffer->width || state->scaler->SourceHeight() != buffer->height; + std::uint32_t layer = 0; + return PresentableResident(graphicsContext(), *buffer, filter, pending, convert, layer) == nullptr; } void VulkanDevice::FlipBatches(std::uint64_t& submissions, std::uint64_t& unsignaled) const { @@ -3120,7 +3186,13 @@ Graphics::Context VulkanDevice::buildContext() const { state->depthClamp, state->hostImportAlignment }; + context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; context.dmaBufImport = state->dmaBufImport; + context.sparseBinding = state->sparseBinding; + context.queueFamily = state->queueFamily; + context.sparseFamily = state->sparseFamily; + context.sparseQueue = state->sparseQueue; + context.sparseQueueMutex = &state->sparseQueueMutex; context.recorder = state->recorder.get(); context.descriptorCache = state->descriptorCache.get(); context.samplerCache = state->samplerCache.get(); @@ -4248,7 +4320,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; - capture.spirv = shader.spirv; + capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; const auto layoutBindings = resources->LayoutBindings(); @@ -4275,7 +4347,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); } if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index c586948030..f8acde495e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -18,12 +18,13 @@ enum class ColorTileMode : std::uint32_t { Standard64KB = 9, S4KBX = 0x15, D4KBX = 0x16, + Z64KBX = 0x18, D64KBX = 0x1a, RenderTarget = 0x1b }; constexpr bool ColorTileModeIsXor(ColorTileMode mode) { - return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; + return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::Z64KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; } constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { @@ -31,7 +32,7 @@ constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { } constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) { - return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : TextureTileMode::kR64KBX; + return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : mode == ColorTileMode::S4KBX ? TextureTileMode::kS4KBX : mode == ColorTileMode::D4KBX ? TextureTileMode::kD4KBX : mode == ColorTileMode::D64KBX ? TextureTileMode::kD64KBX : mode == ColorTileMode::Z64KBX ? TextureTileMode::kZ64KBX : TextureTileMode::kR64KBX; } ColorTileMode DecodeColorTileMode(std::uint32_t attrib3); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 68ba7ea944..5f907e3404 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -4,11 +4,14 @@ #ifndef VK_NO_PROTOTYPES #define VK_NO_PROTOTYPES #endif +#include #include #include #include #include #include +#include +#include #include #include #include @@ -28,6 +31,19 @@ class SamplerCache; class ShaderResources; class PlaceholderImages; +class PipelineCacheSynchronization { +public: + using PipelineUseLock = std::shared_lock; + using SnapshotLock = std::unique_lock; + + PipelineUseLock LockForPipelineUse() const { return PipelineUseLock(mutex); } + SnapshotLock LockForSnapshot() const { return SnapshotLock(mutex); } + SnapshotLock TryLockForSnapshot() const { return SnapshotLock(mutex, std::try_to_lock); } + +private: + mutable std::shared_mutex mutex; +}; + inline void Require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error("AGC graphics: " + reason); } @@ -123,6 +139,11 @@ struct Context { // Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment. VkDeviceSize hostImportAlignment = 0; bool dmaBufImport = false; + bool sparseBinding = false; + std::uint32_t queueFamily = 0; + std::uint32_t sparseFamily = VK_QUEUE_FAMILY_IGNORED; + VkQueue sparseQueue = VK_NULL_HANDLE; + std::mutex* sparseQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; @@ -176,6 +197,11 @@ struct Context { bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; VkSampleCountFlags sampleLocationSampleCounts = 0; + PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; + + PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { + return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; + } template TFunction Function(const char* name) const { @@ -200,6 +226,33 @@ struct Context { throw std::runtime_error("AGC graphics: required Vulkan memory type is unavailable"); } + std::uint32_t BufferMemoryType(std::uint32_t mask, VkMemoryPropertyFlags flags, VkDeviceSize bytes) const { + constexpr VkMemoryPropertyFlags coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + auto type = MemoryType(mask, flags); + if (flags == coherent) { + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + if ((mask & (1u << i)) != 0 && (memory.memoryTypes[i].propertyFlags & (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) == (coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT)) { + type = i; + break; + } + } + } + if ((flags & coherent) != coherent || memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + memoryProperties2(physical, &properties); + const auto heap = memory.memoryTypes[type].heapIndex; + const auto fits = [&](std::uint32_t candidate) { + return reported.heapBudget[candidate] != 0 && reported.heapUsage[candidate] < reported.heapBudget[candidate] && bytes <= reported.heapBudget[candidate] - reported.heapUsage[candidate]; + }; + if (reported.heapBudget[heap] == 0 || fits(heap)) return type; + for (std::uint32_t i = 0; i < memory.memoryTypeCount; ++i) { + const auto& candidate = memory.memoryTypes[i]; + if ((mask & (1u << i)) != 0 && (candidate.propertyFlags & flags) == flags && candidate.heapIndex != heap && fits(candidate.heapIndex)) return i; + } + return type; + } + std::uint32_t DeviceMemoryType(std::uint32_t mask, VkDeviceSize bytes, const char* what) const { const auto type = MemoryType(mask, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return type; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index 246cc3ba94..672a396493 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include namespace AgcDriver::Graphics { @@ -13,8 +14,8 @@ public: GpuColorTransfer(const GpuColorTransfer&) = delete; GpuColorTransfer& operator=(const GpuColorTransfer&) = delete; void Upload(std::uint64_t address, std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes = 4); - void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false); - void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false); + void Detile(VkCommandBuffer commands, bool swapRedBlue = false, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); + void DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t width, std::uint32_t height, ColorTileMode mode, bool swapRedBlue, bool tenBit, bool halfFloat = false, std::uint32_t layer = 0); void Tile(VkCommandBuffer commands); void WriteBack(std::uint64_t address); VkBuffer LinearBuffer() const; @@ -22,8 +23,8 @@ public: private: void prepare(std::uint32_t width, std::uint32_t height, ColorTileMode mode, std::uint32_t elementBytes); - void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false, bool halfFloat = false); - void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat); + void convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit = false, bool halfFloat = false, std::optional pqWhiteNits = std::nullopt); + void dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits); void release() noexcept; Context context; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; @@ -36,6 +37,7 @@ private: std::unique_ptr tiledDevice; std::unique_ptr linear; std::unique_ptr thresholds; + std::unique_ptr pqNits; std::uint32_t width = 0; std::uint32_t height = 0; ColorTileMode mode = ColorTileMode::Linear; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 01c754b506..2bcdbbbca0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -33,7 +33,9 @@ struct HostImport { std::uint64_t serial = 0; bool unwatched = false; bool dmaBuf = false; + bool joined = false; std::shared_ptr chunk; + mutable std::uint64_t lastUse = 0; }; enum class ImportWatch : std::uint8_t { Watch, Unwatch }; @@ -58,6 +60,10 @@ void SetImportWatch(const Context& context, ImportWatch watch); // the import's buffer. const HostImport* HostImportFor(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 HostImportLiveBytes(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Whether an existing import covers [address, address + bytes), without reconciling the imports // with the registry or making one (HostImportFor may take a registry lease): a hint for choices @@ -345,7 +351,7 @@ private: // The region of a descriptor-bound range (AddWritable/AddReadable), committed pages only. A // range inside a base region of the space adds nothing: the region serves it, as today's merge // of the two did. - void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept); + void addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly); static bool servesView(const Region& region, std::uint64_t view); static bool sparseHeapCandidate(const Region& region); bool bindSparseMirror(Region& region, const GuestAllocations::Lease& lease); @@ -378,11 +384,13 @@ private: // Import registry epoch when `direct` pointers were taken at acquire time; they are reused while // no import was destroyed since. std::uint64_t importsEpoch = 0; + std::uint64_t importFloor = 0; std::vector regions; // Whether `regions` is in ascending address order (true right after AcquireRegistered, whose // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; + std::vector> readsOverBase; std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 4ccd6a4c3b..997e9dc7e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -93,6 +93,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); 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 50674497b0..bfcc8fd534 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -50,6 +50,7 @@ bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, V bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); +bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void ClearMultisampleTargets(VkDevice device); void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 54727e9459..6398fde6d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -21,6 +21,7 @@ public: PipelineCache(const PipelineCache&) = delete; PipelineCache& operator=(const PipelineCache&) = delete; VkPipelineCache Handle() const { return cache; } + PipelineCacheSynchronization& Synchronization() { return synchronization; } private: void load(std::vector& initialData); @@ -30,6 +31,7 @@ private: Context context; VkPhysicalDeviceProperties properties{}; VkPipelineCache cache = VK_NULL_HANDLE; + PipelineCacheSynchronization synchronization; std::filesystem::path path; std::size_t savedBytes = 0; std::mutex mutex; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 7e5defb334..1fda73efd9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -741,7 +741,7 @@ private: static const Batch::Read* readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end); bool signaled(const Batch& batch) const; // Rebuilds the lock-free snapshot of pending writes from open, inFlight and finishing. - void publishPendingWrites() const; + void publishPendingWrites(std::span> additions = {}) const; // Appends one range to the open batch; returns whether the snapshot must be rebuilt for it. // `ownLabel`: the range is a label's own store (NoteLabel, AfterCompletions), which does not // overwrite the table entries it covers; any other range flags them (table mutex, briefly). @@ -825,6 +825,8 @@ private: std::map indexRanges; std::list indexRangeRecency; bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes); + mutable std::weak_ptr publishedWriteRanges; + mutable bool publishedWriteRangesValid = false; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index 76804ccfc3..d7749e2da3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -66,7 +66,7 @@ private: }; void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span> samplers); -void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); +bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers); void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers); void RequireNonSeamlessCube(bool cube, std::uint32_t samplerMask, std::span> samplers); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 51d1b86941..1fa042481d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -433,6 +433,7 @@ private: Context context; std::vector layoutKey; GuestBufferMemory guestMemory; + std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; bool usesFaultBuffer = false; @@ -466,6 +467,7 @@ private: std::vector> textures; std::vector textureFirstLayer; std::vector textureBaseLevel; + std::vector textureSrgb; std::vector> storageTextures; std::vector storageMips; std::vector storageKeys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4ac57ebdca..b3e056cd72 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -114,6 +114,7 @@ struct State { bool negativeOneToOne; bool depthClamp = false; bool alphaToCoverage = false; + bool ditheredAlphaToCoverage = false; VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT; VkRect2D scissor; VkCullModeFlags cullMode; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index e73d226c75..0524592a6c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -66,6 +66,9 @@ public: Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); + struct BlockCopyTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag); + static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; } bool RefreshDepthCopy(); struct DepthCopyCounts { @@ -81,6 +84,8 @@ public: VkImageView View() const; VkImageView FirstLayerView() const { return firstLayerView; } VkImageView BaseLevelView() const; + bool HasSrgbView() const { return srgbInfo.has_value(); } + VkImageView SrgbView(bool firstLayer, bool baseLevel) const; struct ViewRange { VkImageViewType type; std::uint32_t levels; @@ -106,8 +111,10 @@ public: private: void release() noexcept; + bool refreshBlockCopy(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); + void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); // Held by value: cached textures outlive the Context of the draw that created them. Context context; @@ -122,6 +129,10 @@ private: std::optional baseLevelInfo; mutable std::mutex baseLevelMutex; mutable VkImageView baseLevelView = VK_NULL_HANDLE; + std::optional srgbInfo; + float srgbMinLod = 0.0f; + mutable std::mutex srgbMutex; + mutable std::array srgbViews{}; VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkDeviceSize allocationBytes = 0; VkDeviceSize countedBytes = 0; @@ -133,10 +144,12 @@ private: bool copiedOnce = false; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; + VkFormat blockFormat = VK_FORMAT_UNDEFINED; std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; }; VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); +VkFormat SrgbTwinFormat(const Context& context, VkFormat format); // A guest texture a shader writes through a storage image. It is uploaded like a sampled texture; // after the GPU work completes its results are stored to guest memory (retiled, changed bytes only), @@ -169,6 +182,7 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); + void MarkSliceDirty(std::uint32_t slice); void Flush(); // Stores every pending image overlapping the range, except `except`; returns whether any was. // Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards. @@ -179,6 +193,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 void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); @@ -254,6 +269,7 @@ public: // contains a descriptor of its first mips), if any; the caller decides whether the geometry fits // (Texture::CanCopyFrom, ResidentPresentable). static std::shared_ptr FindPending(std::uint64_t address, std::uint64_t bytes); + static std::shared_ptr FindPendingSlice(std::uint64_t address, std::uint64_t bytes); // How a buffer fill of [address, address + bytes) (Driver.cpp's fill HLE) meets the storage // images alive: none of them; a whole surface (Exact) or one array layer of a surface (Layer), // `image` and `layer` naming it, whatever other images lie over the range (`others`, of which @@ -288,7 +304,7 @@ public: // captures, snapshots) takes the bytes through the flush hook, and this image alone is current. // False, naming why in `refusal`, when the fill must be stored as before. static constexpr std::uint32_t WholeImage = ~0u; - bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal); + bool FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear = false); // The cached, live image whose surface is exactly [address, address + bytes), if any (the newest // of several: see ClassifyFill). static std::shared_ptr FindLive(std::uint64_t address, std::uint64_t bytes); @@ -327,7 +343,9 @@ public: bool Refresh(); static std::uint64_t SinglePassMoves(); static std::uint64_t RefreshesProved(); + static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; + std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } private: @@ -348,6 +366,12 @@ private: void writeBack(std::uint64_t address, std::size_t bytes); void writeBackLayers(const std::vector& layers); bool unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const; + void saveBeforeUnwatch(); + void forgetKeptAtUnwatch(std::uint32_t unit); + bool keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const; + // Drops the saved guest bytes of the units over [address, address + bytes) (another image's fill + // or copy supersedes them there); the other units' bytes stay valid. + void forgetSavedOver(std::uint64_t address, std::size_t bytes); bool guestBytesSettled() const; // Tracked units as 64 KiB write-stamp blocks (`blockUnits`: a thin tiled surface at a 64 KiB // aligned base; APS5_NO_BLOCK_TRACKING=1 tracks array layers as above instead): a fill of one @@ -483,7 +507,7 @@ private: std::uint64_t sliceLinearBytes = 0; SurfaceGeometry geometry; std::vector original; - mutable std::array comparedGuestBytes{}; + mutable std::array comparedGuestBytes{}; std::vector generationBaseline; // DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value. DccKeys uploadedKeys = DccKeys::Uncompressed; @@ -546,6 +570,7 @@ private: std::uint64_t version = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; + std::map keptAtUnwatch; // The storage cache let the image go (Flush): a lookup makes a new image of the surface, so // no fill may put results into this one (guarded by the live-image registry lock). bool released = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp index c494b7aee9..7e787cb840 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/ColorTransfer.comp @@ -1,14 +1,19 @@ #version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" layout(local_size_x = 8, local_size_y = 8) in; layout(set = 0, binding = 0, std430) buffer TiledPixels { uint tiled[]; }; layout(set = 0, binding = 1, std430) buffer LinearPixels { uint linear[]; }; layout(set = 0, binding = 2, std430) readonly buffer ScanoutThresholds { uint thresholds[]; }; +layout(set = 0, binding = 3, std430) readonly buffer PqLuminance { float pqNits[]; }; layout(push_constant) uniform Parameters { uint width; uint height; uint blocksPerRow; uint mode; + float whiteNits; } parameters; uint scanoutCode(uint bits, uint base) { @@ -45,6 +50,13 @@ void main() { } else { pixel = (parameters.mode & 1u) != 0u ? linear[linearIndex] : tiled[tiledIndex]; } + if ((parameters.mode & 32u) != 0u) { + vec3 nits = vec3(pqNits[pixel & 0x3ffu], pqNits[(pixel >> 10u) & 0x3ffu], pqNits[(pixel >> 20u) & 0x3ffu]); + vec3 encoded = srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)); + uvec3 codes = uvec3(floor(encoded * 255.0 + 0.5)); + linear[linearIndex] = codes.b | (codes.g << 8u) | (codes.r << 16u) | 0xff000000u; + return; + } // Mode 8: A2B10G10R10 source (red in the low bits) presented as 8-bit RGBA. if ((parameters.mode & 8u) != 0u) pixel = ((pixel >> 2u) & 0xffu) | (((pixel >> 12u) & 0xffu) << 8u) | (((pixel >> 22u) & 0xffu) << 16u) | 0xff000000u; if ((parameters.mode & 2u) != 0u) pixel = (pixel & 0xff00ff00u) | ((pixel & 0xffu) << 16u) | ((pixel >> 16u) & 0xffu); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl new file mode 100644 index 0000000000..4a35119dea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PqToSdr.glsl @@ -0,0 +1,28 @@ +float pqToneMap(float luminance, float relativeWhite) { + const float knee = 0.75; + if (luminance <= knee) return luminance; + float t = (luminance - knee) / (1.0 - knee); + float w = (relativeWhite - knee) / (1.0 - knee); + return knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); +} + +vec3 pqToSdrLinear(vec3 nits, float whiteNits) { + float referenceWhite = min(203.0, whiteNits); + vec3 bt2020 = nits / referenceWhite; + vec3 rgb = max(vec3( + 1.6604910 * bt2020.r - 0.5876411 * bt2020.g - 0.0728499 * bt2020.b, + -0.1245505 * bt2020.r + 1.1328999 * bt2020.g - 0.0083494 * bt2020.b, + -0.0181508 * bt2020.r - 0.1005789 * bt2020.g + 1.1187297 * bt2020.b), vec3(0.0)); + float luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b; + if (luminance > 0.0) rgb = min(rgb * (pqToneMap(luminance, whiteNits / referenceWhite) / luminance), vec3(1.0)); + return rgb; +} + +float srgbEncode(float linear) { + float v = clamp(linear, 0.0, 1.0); + return v <= 0.0031308 ? 12.92 * v : 1.055 * pow(v, 1.0 / 2.4) - 0.055; +} + +vec3 srgbEncode(vec3 linear) { + return vec3(srgbEncode(linear.r), srgbEncode(linear.g), srgbEncode(linear.b)); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag new file mode 100644 index 0000000000..ccd35c25ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.frag @@ -0,0 +1,62 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require + +#include "PqToSdr.glsl" + +layout(set = 0, binding = 0) uniform usampler2D source; +layout(set = 0, binding = 1, std430) readonly buffer PqLuminance { float pqNits[]; }; +layout(push_constant) uniform Parameters { + ivec2 origin; + ivec2 size; + ivec2 sourceSize; + int stride; + uint mode; + float whiteNits; +} parameters; +layout(location = 0) out vec4 color; + +vec4 decode(uint word) { + if ((parameters.mode & 16u) != 0u) { + vec3 nits = vec3(pqNits[word & 0x3ffu], pqNits[(word >> 10u) & 0x3ffu], pqNits[(word >> 20u) & 0x3ffu]); + return vec4(srgbEncode(pqToSdrLinear((parameters.mode & 2u) != 0u ? nits : nits.zyx, parameters.whiteNits)), 1.0); + } + uvec4 channels; + float levels = 255.0; + if ((parameters.mode & 1u) != 0u) { + channels = uvec4(word & 0x3ffu, (word >> 10u) & 0x3ffu, (word >> 20u) & 0x3ffu, 0u); + if ((parameters.mode & 4u) != 0u) levels = 1023.0; + else channels >>= 2u; + channels.w = uint(levels); + } else { + channels = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); + } + vec4 value = vec4(channels) / levels; + return (parameters.mode & 2u) != 0u ? value : value.zyxw; +} + +vec4 texel(ivec2 position) { + return decode(texelFetch(source, clamp(position, ivec2(0), parameters.sourceSize - 1), 0).x); +} + +void main() { + ivec2 pixel = ivec2(gl_FragCoord.xy) - parameters.origin; + if (any(lessThan(pixel, ivec2(0))) || any(greaterThanEqual(pixel, parameters.size))) { + color = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + if ((parameters.mode & 8u) != 0u) { + color = texel(pixel * parameters.stride); + return; + } + if (parameters.size == parameters.sourceSize) { + color = texel(pixel); + return; + } + vec2 position = (vec2(pixel) + 0.5) * vec2(parameters.sourceSize) / vec2(parameters.size) - 0.5; + vec2 base = floor(position); + vec2 weight = position - base; + ivec2 corner = ivec2(base); + vec4 top = mix(texel(corner), texel(corner + ivec2(1, 0)), weight.x); + vec4 bottom = mix(texel(corner + ivec2(0, 1)), texel(corner + ivec2(1, 1)), weight.x); + color = mix(top, bottom, weight.y); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert new file mode 100644 index 0000000000..8b3c0059dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PresentationPass.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + 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/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 699325b2e5..6d1f9ec2fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -47,7 +47,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) { require((attrib3 & 0x80002000u) == 0 && ((resourceType == 0 && (attrib3 & 0x1fffu) == 0) || resourceType == 1 || resourceType == 2) && ((attrib3 >> 27u) & 7u) == 1, "AGC graphics: unsupported color depth, dimension, resource level or metadata mode"); const auto mode = (attrib3 >> 14u) & 0x1fu; const auto fmaskMode = (attrib3 >> 19u) & 0x1fu; - require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); + require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || mode == 0x18 || mode == 0x1a || mode == 0x1b, "AGC graphics: unsupported color tile mode"); require(fmaskMode == 0 || fmaskMode == 0x18 || fmaskMode == (mode & ~3u), "AGC graphics: unsupported color FMASK swizzle mode"); require(resourceType != 0 || mode == 0 || mode == 0x1b, "AGC graphics: 1D color targets with a standard swizzle mode are invalid"); return static_cast(mode); @@ -74,9 +74,9 @@ struct SwizzleTables { }; const SwizzleTables& equationTables(ColorTileMode mode, std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) { - static std::once_flag once[4][5]; - static SwizzleTables tables[4][5]; - const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : 3u; + static std::once_flag once[5][5]; + static SwizzleTables tables[5][5]; + const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : mode == ColorTileMode::D64KBX ? 2u : mode == ColorTileMode::Z64KBX ? 4u : 3u; const auto index = static_cast(std::countr_zero(bytesPerElement)); std::call_once(once[modeIndex][index], [&] { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); @@ -128,6 +128,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, } case ColorTileMode::S4KBX: case ColorTileMode::D4KBX: + case ColorTileMode::Z64KBX: case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: { const auto log2Elements = static_cast(std::countr_zero(ColorTileModeBlockBytes(mode))) - static_cast(std::countr_zero(bytesPerElement)); @@ -138,7 +139,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, const auto& tables = equationTables(mode, bytesPerElement, blockWidth, blockHeight); xOffsets = tables.x.data(); yOffsets = tables.y.data(); - if (mode == ColorTileMode::RenderTarget) equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); + if (mode == ColorTileMode::RenderTarget || mode == ColorTileMode::Z64KBX) equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); break; } case ColorTileMode::Standard4KB: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index e1b0ba0099..ba5bb1d042 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -293,6 +293,48 @@ void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys) { namespace { +struct LandedKeys { + int value = -1; + bool mixed = false; + bool unknown = false; +}; + +LandedKeys KeysAfterPendingWrites(const Recorder& recorder, std::uint64_t begin, std::size_t count) { + auto writes = recorder.PendingWritesOver(begin, count); + std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); + std::vector> unwritten{{begin, begin + count}}; + LandedKeys landed; + const auto see = [&](int key) { + if (landed.value < 0) landed.value = key; + else if (landed.value != key) landed.mixed = true; + }; + for (const auto& write : writes) { + if (landed.mixed || unwritten.empty()) break; + std::vector> rest; + bool covered = false; + for (const auto& [from, to] : unwritten) { + if (write.end <= from || to <= write.begin) { + rest.emplace_back(from, to); + continue; + } + covered = true; + if (from < write.begin) rest.emplace_back(from, write.begin); + if (write.end < to) rest.emplace_back(write.end, to); + } + unwritten = std::move(rest); + if (!covered) continue; + if (write.value >= 0) see(write.value); + else landed.unknown = true; + } + for (const auto& [from, to] : unwritten) { + if (landed.mixed) break; + const auto* keys = reinterpret_cast(from); + if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); + else landed.mixed = true; + } + return landed; +} + std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, std::size_t count) { const auto end = begin + count; std::uint64_t note = 0; @@ -308,8 +350,16 @@ std::optional PendingStoreKeys(Recorder& recorder, std::uint64_t begin, break; } } - if (note == 0 || AnyShadowedOverlaps(begin, count) || recorder.NewestWriteNote(begin, count) != note) return std::nullopt; - return keys; + if (AnyShadowedOverlaps(begin, count)) { + PublishShadow(begin, count, PublishScope::Whole, PublishReason::Keys); + if (AnyShadowedOverlaps(begin, count)) return std::nullopt; + } + if (note != 0 && recorder.NewestWriteNote(begin, count) == note) return keys; + if (!GuestMemory::Accessible(reinterpret_cast(begin), count)) return std::nullopt; + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + if (landed.mixed) return DccKeys::Mixed; + if (landed.unknown || landed.value < 0) return std::nullopt; + return ByteKeys(static_cast(landed.value)); } // Whether the keys already read as uncompressed: scans the same bytes as ReadDccKeys, so it counts @@ -465,12 +515,14 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized return uniform ? ByteKeys(first) : DccKeys::Mixed; } +DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized); + DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) { memoized = false; if (resource.dccAddress == 0) return DccKeys::Uncompressed; DccKeys keys; if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known; - else keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); + else keys = currentDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); if (keys == DccKeys::Uncompressed || (keys == DccKeys::ClearSingle && SingleKeysReadable(resource))) return keys; if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented"); std::byte probe[16]{}; @@ -695,38 +747,8 @@ DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureResource& resource, std::size_t count) { const auto begin = resource.dccAddress; if (!GuestMemory::Accessible(reinterpret_cast(begin), count) || AnyShadowedOverlaps(begin, count)) return false; - auto writes = recorder.PendingWritesOver(begin, count); - std::sort(writes.begin(), writes.end(), [](const Recorder::PendingWrite& a, const Recorder::PendingWrite& b) { return a.note > b.note; }); - std::vector> unwritten{{begin, begin + count}}; - int value = -1; - bool mixed = false; - const auto see = [&](int key) { - if (value < 0) value = key; - else if (value != key) mixed = true; - }; - for (const auto& write : writes) { - if (mixed || unwritten.empty()) break; - std::vector> rest; - bool covered = false; - for (const auto& [from, to] : unwritten) { - if (write.end <= from || to <= write.begin) { - rest.emplace_back(from, to); - continue; - } - covered = true; - if (from < write.begin) rest.emplace_back(from, write.begin); - if (write.end < to) rest.emplace_back(write.end, to); - } - unwritten = std::move(rest); - if (covered && write.value >= 0) see(write.value); - } - for (const auto& [from, to] : unwritten) { - if (mixed) break; - const auto* keys = reinterpret_cast(from); - if (AllKeysEqual(keys + 1, static_cast(to - from) - 1, keys[0])) see(keys[0]); - else mixed = true; - } - return mixed || (value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(value))) == DccKeys::Uncompressed); + const auto landed = KeysAfterPendingWrites(recorder, begin, count); + return landed.mixed || (landed.value >= 0 && EncodedKeys(resource, ByteKeys(static_cast(landed.value))) == DccKeys::Uncompressed); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 9337c6ef16..0e0edce359 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -143,6 +143,24 @@ public: return texture; } + void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { + pendingClear &= ~static_cast(aspect); + 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(); + constexpr VkAccessFlags all = 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, all, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkImageCopy region{}; + region.srcSubresource = {static_cast(aspect), 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(commands, source.image, VK_IMAGE_LAYOUT_GENERAL, 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, all); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } + bool Holds(const Texture* texture) const { return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; }); } @@ -153,19 +171,17 @@ public: float clearDepth = 0.0f; std::uint8_t clearStencil = 0; - std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { - if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr; + bool StorageViewMatches(std::span words, const GuestTextureResource& resource) const { + if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if (ResolveTextureFormat(resource.format) != expected && !depthBits) return nullptr; - const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0; - if (resource.dimension != TextureDimension::k2D && !oneLayer) { - char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); - throw std::runtime_error(text); - } + return ResolveTextureFormat(resource.format) == expected || depthBits; + } + + void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const { + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented"); if (!stencil && hostFormat != target.format) { char text[224]; @@ -177,6 +193,18 @@ public: std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast(target.address)); throw std::runtime_error(text); } + } + + std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { + if (!StorageViewMatches(words, resource)) return nullptr; + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool oneLayer = resource.dimension == TextureDimension::k2DArray && resource.baseArray == 0 && resource.depthOrLastArray == 0; + if (resource.dimension != TextureDimension::k2D && !oneLayer) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); + throw std::runtime_error(text); + } + RequireStorable(resource, mip); ApplyFastClear(); auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) { @@ -390,7 +418,7 @@ private: regions.push_back(region(layer, aspect(), layer)); } if (recorder != nullptr) { - for (const auto& upload : uploads) recorder->Keep(upload); + for (const auto& upload : uploads) recorder->Keep(upload, upload->Bytes().size()); } } VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; @@ -439,6 +467,82 @@ std::map(bytes), nullptr, "storage image creation", PublishScope::None); + storage = StorageTexture::Create(context, *context.detiler, resource, 0); + Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers"); + staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + } + + void Copy(std::span slices, bool toStorage) { + if (slices.empty()) return; + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + const auto commands = recorder->Commands(); + recorder->Keep(storage); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + std::vector storageRegions; + const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspect, std::uint32_t imageLayer) { + VkBufferImageCopy copy{}; + copy.bufferOffset = sliceBytes() * layer; + copy.imageSubresource = {aspect, 0, imageLayer, 1}; + copy.imageExtent = {extent.width, extent.height, 1}; + return copy; + }; + for (const auto& slice : slices) storageRegions.push_back(region(slice.layer, VK_IMAGE_ASPECT_COLOR_BIT, slice.layer)); + const auto copyToBuffer = context.Function("vkCmdCopyImageToBuffer"); + const auto copyToImage = context.Function("vkCmdCopyBufferToImage"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + if (toStorage) { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToBuffer(commands, slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©); + } + } else { + copyToBuffer(commands, storage->Image(), VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), static_cast(storageRegions.size()), storageRegions.data()); + } + 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); + if (toStorage) { + copyToImage(commands, staging->Handle(), storage->Image(), VK_IMAGE_LAYOUT_GENERAL, static_cast(storageRegions.size()), storageRegions.data()); + } else { + for (const auto& slice : slices) { + const auto copy = region(slice.layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + copyToImage(commands, staging->Handle(), slice.surface->image, VK_IMAGE_LAYOUT_GENERAL, 1, ©); + } + } + 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); + Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + recorder->EndGpuTiming(timing, sliceBytes() * slices.size()); + } + + const Context context; + const DepthTarget base; + const VkExtent2D extent; + const std::uint32_t bytesPerTexel; + const std::uint32_t layers; + std::shared_ptr storage; + +private: + std::unique_ptr staging; + + VkDeviceSize sliceBytes() const { + return static_cast(extent.width) * extent.height * bytesPerTexel; + } +}; + +std::map, std::unique_ptr>& layeredStorages() { + static auto* storages = new std::map, std::unique_ptr>(); + return *storages; +} + 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; } @@ -453,6 +557,64 @@ std::vector>& surfaces() { return *list; } +struct PlaneOwner { + DepthSurface* surface = nullptr; + std::uint64_t serial = 0; +}; + +using PlaneOwners = std::map, PlaneOwner>; + +std::uint64_t planeBinds = 0; + +PlaneOwners& depthOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +PlaneOwners& stencilOwners() { + static auto* owners = new PlaneOwners(); + return *owners; +} + +void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address, VkImageAspectFlagBits aspect) { + auto& entry = owners[{surface.context.device, address}]; + auto* owner = entry.surface; + if (owner != nullptr && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) { + if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect; + else surface.CopyAspectFrom(*owner, aspect); + } + entry = {&surface, ++planeBinds}; +} + +void takePlanes(DepthSurface& surface) { + takePlane(surface, depthOwners(), surface.target.address, VK_IMAGE_ASPECT_DEPTH_BIT); + if (surface.target.stencilAddress != 0) takePlane(surface, stencilOwners(), surface.target.stencilAddress, VK_IMAGE_ASPECT_STENCIL_BIT); +} + +DepthSurface* depthSlice(VkDevice device, std::uint64_t address, const DepthTarget& base) { + if (const auto found = depthOwners().find({device, address}); found != depthOwners().end()) { + const auto& target = found->second.surface->target; + if (target.extent.width == base.extent.width && target.extent.height == base.extent.height && target.format == base.format) return found->second.surface; + } + const auto& list = surfaces(); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); + return slice == list.rend() ? nullptr : slice->get(); +} + +DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { + const auto depth = depthOwners().find({device, address}); + const auto stencil = stencilOwners().find({device, address}); + const bool hasDepth = depth != depthOwners().end(); + const bool hasStencil = stencil != stencilOwners().end(); + if (hasDepth && (!hasStencil || depth->second.serial > stencil->second.serial)) return depth->second.surface; + if (hasStencil) return stencil->second.surface; + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); + }); + return found == list.rend() ? nullptr : found->get(); +} + std::set& clearedHtiles() { static auto* cleared = new std::set(); return *cleared; @@ -509,21 +671,27 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; - for (const auto& surface : surfaces()) { - if (surface->context.device != context.device || !sameSurface(surface->target, target)) continue; - surface->clearDepth = target.clearDepth; - surface->clearStencil = target.clearStencil; - if (cleared) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; - surface->ApplyFastClear(); - surface->written = ++depthWrites; - drawnDepth = surface.get(); - return surface->view; + DepthSurface* surface = nullptr; + for (const auto& candidate : surfaces()) { + if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { + surface = candidate.get(); + break; + } } - surfaces().push_back(std::make_unique(context, target)); - surfaces().back()->clearDepth = target.clearDepth; - surfaces().back()->clearStencil = target.clearStencil; - drawnDepth = surfaces().back().get(); - return surfaces().back()->view; + const bool created = surface == nullptr; + if (created) { + surfaces().push_back(std::make_unique(context, target)); + surface = surfaces().back().get(); + } + const bool inherits = depthOwners().contains({context.device, target.address}); + surface->clearDepth = target.clearDepth; + surface->clearStencil = target.clearStencil; + takePlanes(*surface); + if (cleared && (!created || inherits)) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->ApplyFastClear(); + if (!created || inherits) surface->written = ++depthWrites; + drawnDepth = surface; + return surface->view; } std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { @@ -573,8 +741,11 @@ void NoteHtileDepthClear(std::uint64_t htileAddress) { void ClearDepthSurfaces(VkDevice device) { std::lock_guard lock(surfacesMutex()); drawnDepth = nullptr; + std::erase_if(depthOwners(), [&](const auto& entry) { return entry.first.first == device; }); + std::erase_if(stencilOwners(), [&](const auto& entry) { return entry.first.first == device; }); std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); std::erase_if(planeCopies(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); + std::erase_if(layeredStorages(), [&](const auto& entry) { return std::get<0>(entry.first) == device; }); } std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { @@ -587,12 +758,9 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spancontext.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); - }); - if (found == list.rend()) return nullptr; - const auto& base = (*found)->target; + auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return nullptr; + const auto& base = found->target; if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr; const bool stencil = base.stencilAddress != 0 && resource.baseAddress == base.stencilAddress; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; @@ -601,11 +769,11 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanApplyFastClear(); + found->ApplyFastClear(); const bool cube = resource.dimension == TextureDimension::kCube; const bool layered = cube || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray != 0); - if (stencil || (!layered && !integer)) return (*found)->Sampled(words, resource, components); - Require((*found)->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); + if (stencil || (!layered && !integer)) return found->Sampled(words, resource, components); + Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { @@ -616,10 +784,10 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span slices; for (std::uint32_t layer = 0; layer < layers; ++layer) { const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); - const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format; }); - if (slice != list.rend()) (*slice)->ApplyFastClear(); - if (slice == list.rend()) slices.push_back({}); - else slices.push_back({(*slice)->image, slice->get() == drawnDepth ? 0u : (*slice)->written}); + auto* slice = depthSlice(context.device, address, base); + if (slice != nullptr) slice->ApplyFastClear(); + if (slice == nullptr) slices.push_back({}); + else slices.push_back({slice->image, slice == drawnDepth ? 0u : slice->written}); } const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT; auto& copy = planeCopies()[{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}]; @@ -633,11 +801,8 @@ bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resou if (resource.dimension != TextureDimension::k2D || resource.baseArray != 0) return false; const auto address = resource.baseAddress; std::lock_guard lock(surfacesMutex()); - const auto& list = surfaces(); - const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == address || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == address)); - }); - return found != list.rend() && (*found)->Holds(texture); + const auto* found = planeSurface(context.device, address); + return found != nullptr && found->Holds(texture); } VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile) { @@ -668,12 +833,9 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource) { std::lock_guard lock(surfacesMutex()); - const auto& list = surfaces(); - const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { - return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); - }); - if (found == list.rend()) return false; - const auto& target = (*found)->target; + const auto* found = planeSurface(context.device, resource.baseAddress); + if (found == nullptr) return false; + const auto& target = found->target; if (resource.width != target.extent.width || resource.height != target.extent.height) return false; const bool stencil = target.stencilAddress != 0 && target.stencilAddress == resource.baseAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; @@ -682,17 +844,71 @@ bool DepthSurfaceAt(const Context& context, std::span words return ResolveTextureFormat(resource.format) == expected || depthBits; } +namespace { + +std::vector layeredSlices(const Context& context, const LayeredDepthStorage& storage) { + const auto& base = storage.base; + std::vector slices; + const auto& list = surfaces(); + for (std::uint32_t layer = 0; layer < storage.layers; ++layer) { + const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); + const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); + if (slice != list.rend()) slices.push_back({slice->get(), layer}); + } + return slices; +} + +std::shared_ptr layeredDepthStorage(const Context& context, DepthSurface& surface, const GuestTextureResource& resource, std::uint32_t mip) { + const auto& target = surface.target; + const auto layers = resource.depthOrLastArray + 1u; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytesPerTexel = d16 ? 2u : 4u; + if (resource.baseArray != 0) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx through a 2D array view of slices %u-%u is not implemented", static_cast(target.address), resource.baseArray, resource.depthOrLastArray); + throw std::runtime_error(text); + } + surface.RequireStorable(resource, mip); + const auto geometry = DescribeSurface(resource); + if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast(target.address), static_cast(geometry.GuestLayerOffset(1)), static_cast(DepthSliceBytes(target.extent, bytesPerTexel))); + throw std::runtime_error(text); + } + auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension)}]; + if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, layers); + else static_cast(entry->storage->Refresh()); + const auto slices = layeredSlices(context, *entry); + for (const auto& slice : slices) slice.surface->ApplyFastClear(); + entry->Copy(slices, true); + return entry->storage; +} + +} + std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip) { std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); }); - return found == list.rend() ? nullptr : (*found)->Storage(words, resource, mip); + if (found == list.rend()) return nullptr; + auto& surface = **found; + const bool stencil = surface.target.stencilAddress != 0 && surface.target.stencilAddress == resource.baseAddress; + if (stencil || resource.dimension != TextureDimension::k2DArray || (resource.baseArray == 0 && resource.depthOrLastArray == 0)) return surface.Storage(words, resource, mip); + if (!surface.StorageViewMatches(words, resource)) return nullptr; + return layeredDepthStorage(context, surface, resource, mip); } bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) { std::lock_guard lock(surfacesMutex()); + for (auto& [key, entry] : layeredStorages()) { + if (std::get<0>(key) != context.device || entry->storage.get() != &storage) continue; + const auto slices = layeredSlices(context, *entry); + entry->Copy(slices, false); + for (const auto& slice : slices) slice.surface->written = ++depthWrites; + return true; + } for (const auto& surface : surfaces()) { if (surface->context.device == context.device && surface->StoreBack(storage)) return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 68f487b489..507e1c603a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -94,12 +94,12 @@ std::array clearTexel(const ColorTarget& color, DccKeys keys) { return texel; } -bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal) { +bool clearToTexel(StorageTexture& image, const std::array& texel, std::uint32_t elementBytes, const char*& refusal, bool allowRegisterClear = false) { std::array repeated{}; for (std::size_t offset = 0; offset + elementBytes <= repeated.size(); offset += elementBytes) std::memcpy(repeated.data() + offset, texel.data(), elementBytes); std::array pattern{}; std::memcpy(pattern.data(), repeated.data(), repeated.size()); - return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal); + return image.FillClear(std::span(pattern), StorageTexture::WholeImage, refusal, allowRegisterClear); } void writeTexels(const ColorTarget& color, const std::array& texel) { @@ -666,6 +666,8 @@ bool ValidationKey(const Context& context, std::span shade add(tessellation.partitioning); add(tessellation.outputTopology); add(tessellation.patchesPerGroup); + add(tessellation.primitiveGroupSize); + add(tessellation.breakAtInstance ? 1u : 0u); } } return keyed; @@ -1012,8 +1014,12 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P Require(draw.firstInstance <= std::numeric_limits::max() - (draw.instanceCount - 1u), "auto draw instance range overflow"); } } - if (args == nullptr) Require(draw.indexCount != 0 && draw.instanceCount != 0, "zero-count indexed draws are unsupported"); - else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + if (args == nullptr) { + if (draw.indexCount == 0 || draw.instanceCount == 0) { + inputs.nothing = true; + return inputs; + } + } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); inputs.indexBytes = static_cast(draw.indexCount) * draw.indexSize; const auto indexBytes = inputs.indexBytes; APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast(indexBytes)); @@ -1037,6 +1043,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto& mesh = *state.stages.mesh; const auto inputSize = mesh.inputPrimitive == 1 ? 1u : mesh.inputPrimitive == 2 ? 2u : 3u; Require(draw.indexCount >= inputSize && mesh.primitivesPerGroup != 0, "mesh draw contains no complete primitive"); + Require(((state.stages.registerValue >> 19u) & 3u) == 0 || !draw.indexed, "indexed fast-launch draws are unsupported"); if (args == nullptr) { const auto step = mesh.inputPrimitive == 5 || mesh.inputPrimitive == 6 ? 1u : inputSize; const auto primitives = (draw.indexCount - inputSize) / step + 1u; @@ -1481,7 +1488,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::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); @@ -1516,8 +1523,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); }); else recorder.OnComplete([resources] { resources->WriteBackBuffers(); }); } - for (const auto& resident : residents) { - if (resident != nullptr) resident->MarkDirty(); + for (std::size_t index = 0; index < residents.size(); ++index) { + if (residents[index] == nullptr) continue; + if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); + else residents[index]->MarkDirty(); } } @@ -1532,6 +1541,7 @@ struct RecordedDraw { std::shared_ptr framebuffer; std::span targetViews; std::vector> targets; + std::vector targetSlices; std::vector> proxies; const IndirectRecord* indirect = nullptr; std::span moved; @@ -1782,7 +1792,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); timer.phase(PhaseRecord); - keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); + keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome); timer.phase(PhaseKeep); if (record.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2132,6 +2142,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) { if (binding.proxied) record.proxies.push_back(binding.resident); } @@ -2144,7 +2155,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // 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() && !state.depth && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2163,6 +2174,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->masked = masked; recipe->fragmentOutputs = inputs.fragmentOutputs; recipe->shaderStages = inputs.shaderStages; + recipe->hasDepth = state.depth.has_value(); *recipeOut = std::move(recipe); } timing.Mark("draw_and_resource_release"); @@ -2331,7 +2343,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::move(scratch), std::move(checkRecords), listed, completion, outcome); + 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); timer.phase(PhaseKeep); if (outcome.waited) { // The wait the dispatch path makes for such work (source 3, "address-based"): the @@ -2376,7 +2388,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->MarkDirty(); + binding.resident->MarkSliceDirty(binding.color.arraySlice); continue; } if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original); @@ -2440,7 +2452,9 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con Require(!draw.indirect, "a draw recipe covers direct draws only"); auto* recorder = Recorder::Active(); if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable); - Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state"); + if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone); + const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u); + Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state"); auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe); if (inputs.nothing) { result.recorded = true; @@ -2466,6 +2480,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone); targets.push_back(std::move(resident)); } + if (recipe.hasDepth) { + timer.phase(PhaseSetup); + const auto depthView = DepthSurfaceView(context, *state.depth); + timer.phase(PhaseReadTarget); + if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); + } timer.phase(PhasePrepare); // The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed // key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps @@ -2507,6 +2527,7 @@ 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); record.pushBytes = &recipe.pushBytes; record.pushStages = recipe.pushStages; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); @@ -2561,7 +2582,7 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) const auto resident = metadataPassResident(context, color); if (resident != nullptr) { const char* refusal = nullptr; - const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; + const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal, true) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys; if (current) { resident->MarkDirty(); MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 4aec57b1d8..973aff1e34 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -1,8 +1,11 @@ #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/ColorTransfer_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include +#include +#include #include namespace AgcDriver::Graphics { @@ -10,7 +13,7 @@ namespace AgcDriver::Graphics { GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { VkShaderModule module = VK_NULL_HANDLE; try { - std::array bindings{}; + std::array bindings{}; for (std::uint32_t i = 0; i < bindings.size(); ++i) { bindings[i] = {i, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; } @@ -18,7 +21,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { descriptorInfo.bindingCount = static_cast(bindings.size()); descriptorInfo.pBindings = bindings.data(); Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout color transfer"); - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 16}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 20}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; layoutInfo.setLayoutCount = 1; layoutInfo.pSetLayouts = &descriptorLayout; @@ -35,10 +38,13 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = pipelineLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines color transfer"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; - const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3}; + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4}; VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.maxSets = 1; poolInfo.poolSizeCount = 1; @@ -52,14 +58,20 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { const auto& table = HalfFloatScanoutThresholds(); thresholds = std::make_unique(context, sizeof(table), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memcpy(thresholds->Bytes().data(), table.data(), sizeof(table)); - const VkDescriptorBufferInfo thresholdInfo{thresholds->Handle(), 0, sizeof(table)}; - VkWriteDescriptorSet thresholdWrite{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - thresholdWrite.dstSet = descriptorSet; - thresholdWrite.dstBinding = 2; - thresholdWrite.descriptorCount = 1; - thresholdWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - thresholdWrite.pBufferInfo = &thresholdInfo; - context.Function("vkUpdateDescriptorSets")(context.device, 1, &thresholdWrite, 0, nullptr); + const auto& luminance = AgcDriver::PqEotfNits(); + pqNits = std::make_unique(context, sizeof(luminance), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::memcpy(pqNits->Bytes().data(), luminance.data(), sizeof(luminance)); + const std::array tableInfo{{{thresholds->Handle(), 0, sizeof(table)}, {pqNits->Handle(), 0, sizeof(luminance)}}}; + std::array tableWrites{}; + for (std::uint32_t i = 0; i < tableWrites.size(); ++i) { + tableWrites[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + tableWrites[i].dstSet = descriptorSet; + tableWrites[i].dstBinding = 2 + i; + tableWrites[i].descriptorCount = 1; + tableWrites[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + tableWrites[i].pBufferInfo = &tableInfo[i]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); } catch (...) { if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); release(); @@ -115,27 +127,28 @@ void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std GuestMemory::Read(address, tiled->Bytes(), layout.Alignment()); } -void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool halfFloat) { +void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { Require(commands != VK_NULL_HANDLE && tiled && tiledDevice && linear, "color transfer is not prepared"); Require(halfFloat == (elementBytes == 8u), "color transfer source does not match the prepared texel size"); Require(!toTiled || !halfFloat, "color transfer cannot write 16-bit float texels back to the guest"); // The guest bytes move to video memory with one DMA copy; the shader's scattered accesses stay local. RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CopyBuffer(context, commands, tiled->Handle(), 0, tiledDevice->Handle(), 0, tiledDevice->Size()); - dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget, halfFloat); + dispatch(commands, toTiled, swapRedBlue, tenBit, mode == ColorTileMode::RenderTarget, halfFloat, pqWhiteNits); if (toTiled) { CopyBuffer(context, commands, tiledDevice->Handle(), 0, tiled->Handle(), 0, tiledDevice->Size()); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); } } -void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat) { +void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swapRedBlue, bool tenBit, bool tiledSource, bool halfFloat, std::optional pqWhiteNits) { + Require(!pqWhiteNits || (!toTiled && !tenBit && !halfFloat && elementBytes == 4u && std::isfinite(*pqWhiteNits) && *pqWhiteNits >= 1.0f), "color transfer converts PQ texels of 4 bytes to an 8-bit linear output with a white level of at least 1 nit"); VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; before.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; before.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; const auto barrier = context.Function("vkCmdPipelineBarrier"); barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); - const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u) | (halfFloat ? 16u : 0u)}; + const std::array push{width, height, (width + 127u) / 128u, (toTiled ? 1u : 0u) | (swapRedBlue ? 2u : 0u) | (tiledSource ? 4u : 0u) | (tenBit ? 8u : 0u) | (halfFloat ? 16u : 0u) | (pqWhiteNits ? 32u : 0u), std::bit_cast(pqWhiteNits.value_or(0.0f))}; context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); @@ -146,20 +159,20 @@ void GpuColorTransfer::dispatch(VkCommandBuffer commands, bool toTiled, bool swa barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); } -void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit, bool halfFloat) { +void GpuColorTransfer::DetileImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, std::uint32_t newWidth, std::uint32_t newHeight, ColorTileMode newMode, bool swapRedBlue, bool tenBit, bool halfFloat, std::uint32_t layer) { Require(commands != VK_NULL_HANDLE && image != VK_NULL_HANDLE, "color transfer image source is unavailable"); prepare(newWidth, newHeight, newMode, halfFloat ? 8u : 4u); Require(tiledDevice->Size() >= static_cast(width) * height * elementBytes, "color transfer buffer is smaller than the image"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); VkBufferImageCopy copy{}; - copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; copy.imageExtent = {width, height, 1}; context.Function("vkCmdCopyImageToBuffer")(commands, image, layout, tiledDevice->Handle(), 1, ©); - dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat); + dispatch(commands, false, swapRedBlue, tenBit, false, halfFloat, std::nullopt); } -void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit, bool halfFloat) { - convert(commands, false, swapRedBlue, tenBit, halfFloat); +void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue, bool tenBit, bool halfFloat, std::optional pqWhiteNits) { + convert(commands, false, swapRedBlue, tenBit, halfFloat, pqWhiteNits); } void GpuColorTransfer::Tile(VkCommandBuffer commands) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index f537ad995c..bdb5794498 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -162,6 +162,7 @@ struct HostImports { std::map>> parked; std::uint64_t liveBytes = 0; std::map failed; + std::map failedJoins; std::set unaliasedSpans; // Registry generation the imports were last reconciled with. std::uint64_t refreshedGeneration = 0; @@ -171,6 +172,8 @@ struct HostImports { VkDevice watchDevice = VK_NULL_HANDLE; bool unwatchImports = false; bool unwatchDmaBufImports = false; + std::uint64_t useCounter = 0; + std::uint64_t buildFloor = 0; }; HostImports& Imports() { @@ -211,12 +214,13 @@ struct RetiredImport { const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); void retireImport(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, bool hold = true) { // Results shadowed for the import reach its (old) buffer first where the memory is still a // readable registered range (the import retires because its registration vanished or changed // size); the holder below outlives the batch that copies them. - RetireShadow(device, it->second, [&lease](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end); }); + RetireShadow(device, it->second, [&lease, joined = it->second.joined](std::uint64_t begin, std::uint64_t end) { return runCovers(lease, begin, end) || (joined && joinedCovers(lease, begin, end)); }); ++state.epoch; state.liveBytes -= it->second.bytes; auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it)); @@ -336,6 +340,7 @@ const char* createDmaBufImport(const Context& context, HostImport& entry, int fi #ifndef _WIN32 constexpr std::uint64_t ImportChunkBytes = std::uint64_t{64} << 20u; +constexpr VkBufferUsageFlags ChunkImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; struct ImportChunk { VkDevice device = VK_NULL_HANDLE; @@ -438,7 +443,7 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; info.size = entry.bytes; - info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; + info.usage = ChunkImportUsage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) { @@ -477,6 +482,101 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, entry.dmaBuf = true; return nullptr; } + +constexpr VkBufferUsageFlags SparseImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; + +[[noreturn]] void throwSparseImport(const HostImport& entry, const char* step, VkResult result) { + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: sparse dma-buf import of 0x%llx+0x%llx failed at %s (%d)", static_cast(entry.base), static_cast(entry.bytes), step, static_cast(result)); + throw std::runtime_error(text); +} + +VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const HostImport& entry, VkBufferCreateFlags flags, VkBufferUsageFlags usage, VkDeviceSize size, VkBuffer* kept) { + const VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO, nullptr, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + info.flags = flags; + info.size = size; + info.usage = usage; + const std::uint32_t families[] = {context.queueFamily, context.sparseFamily}; + const bool concurrent = (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0 && context.sparseQueue != VK_NULL_HANDLE; + info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = concurrent ? 2u : 0u; + info.pQueueFamilyIndices = concurrent ? families : nullptr; + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) throwSparseImport(entry, "vkCreateBuffer", result); + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, buffer, &requirements); + if (kept != nullptr) { + *kept = buffer; + } else { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + } + return requirements; +} + +bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; + if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread()) || udmabufDevice() < 0) return false; + struct stat backing {}; + if (fstat(file, &backing) != 0) throwSparseImport(entry, "fstat of the shared backing", VK_ERROR_INVALID_EXTERNAL_HANDLE); + const auto chunkTypes = dmaBufBufferRequirements(context, entry, 0, ChunkImportUsage, ImportChunkBytes, nullptr).memoryTypeBits; + VkBuffer buffer = VK_NULL_HANDLE; + const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, entry.bytes, &buffer); + const auto alignment = requirements.alignment; + const bool chosen = alignment != 0 && offset % alignment == 0 && ImportChunkBytes % alignment == 0 && (chunkTypes & ~requirements.memoryTypeBits) == 0 && (requirements.memoryTypeBits & chunkTypes) != 0 && + (offset + requirements.size + ImportChunkBytes - 1) / ImportChunkBytes * ImportChunkBytes <= static_cast(backing.st_size); + const auto fail = [&](const char* step, VkResult result) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + throwSparseImport(entry, step, result); + }; + if (!chosen) { + context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); + return false; + } + auto chunks = std::make_shared>>(); + std::vector binds; + for (VkDeviceSize resource = 0; resource < requirements.size;) { + const auto position = offset + resource; + const auto index = position / ImportChunkBytes; + const auto within = position - index * ImportChunkBytes; + const auto size = std::min(ImportChunkBytes - within, requirements.size - resource); + const char* step = nullptr; + VkResult failure = VK_SUCCESS; + auto chunk = importChunk(context, file, index, requirements.memoryTypeBits, step, failure); + if (chunk == nullptr) fail(step, failure); + if (((requirements.memoryTypeBits >> chunk->memoryType) & 1u) == 0) fail("the memory type of a chunk import", VK_ERROR_FORMAT_NOT_SUPPORTED); + binds.push_back({resource, size, chunk->memory, within, 0}); + chunks->push_back(std::move(chunk)); + resource += size; + } + const VkSparseBufferMemoryBindInfo bufferBinds{buffer, static_cast(binds.size()), binds.data()}; + VkBindSparseInfo bind{VK_STRUCTURE_TYPE_BIND_SPARSE_INFO}; + bind.bufferBindCount = 1; + bind.pBufferBinds = &bufferBinds; + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &fence); result != VK_SUCCESS) fail("vkCreateFence", result); + VkResult bound = VK_SUCCESS; + VkResult waited = VK_SUCCESS; + { + std::unique_lock queueLock; + if (ownQueue) queueLock = std::unique_lock(*context.sparseQueueMutex); + bound = context.Function("vkQueueBindSparse")(ownQueue ? context.sparseQueue : context.queue, 1, &bind, fence); + if (bound == VK_SUCCESS) waited = context.Function("vkWaitForFences")(context.device, 1, &fence, VK_TRUE, UINT64_MAX); + } + context.Function("vkDestroyFence")(context.device, fence, nullptr); + if (bound != VK_SUCCESS) fail("vkQueueBindSparse", bound); + if (waited != VK_SUCCESS) fail("vkWaitForFences", waited); + const VkBufferDeviceAddressInfo addressInfo{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, nullptr, buffer}; + const auto address = context.Function("vkGetBufferDeviceAddressKHR")(context.device, &addressInfo); + if (address == 0) fail("vkGetBufferDeviceAddressKHR", VK_ERROR_UNKNOWN); + entry.buffer = buffer; + entry.memory = VK_NULL_HANDLE; + entry.address = address; + entry.chunk = std::move(chunks); + entry.dmaBuf = true; + return true; +} #endif #ifndef _WIN32 @@ -496,14 +596,11 @@ bool sharedBacking(const Context& context, const HostImport& entry, BackingFile& } const char* createBackingImport(const Context& context, HostImport& entry, BackingFile& backing, VkResult& failure) { - const int file = std::exchange(backing.file, -1); bool fits = false; VkResult chunkFailure = VK_SUCCESS; - if (createChunkedDmaBufImport(context, entry, file, backing.offset, fits, chunkFailure) == nullptr && fits) { - close(file); - return nullptr; - } - return createDmaBufImport(context, entry, file, backing.offset, failure); + if (createChunkedDmaBufImport(context, entry, backing.file, backing.offset, fits, chunkFailure) == nullptr && fits) return nullptr; + if (!fits && createSparseDmaBufImport(context, entry, backing.file, backing.offset)) return nullptr; + return createDmaBufImport(context, entry, std::exchange(backing.file, -1), backing.offset, failure); } #endif @@ -580,6 +677,13 @@ std::shared_ptr leasedRangeOwner(const GuestAlloc return found != lease.end() && (*found)->address == base ? *found : nullptr; } +std::shared_ptr leasedRangeOwner(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; }); + if (found == lease.begin()) return nullptr; + const auto& range = *std::prev(found); + return begin >= range->address && end <= range->address + range->bytes ? range : nullptr; +} + struct ImportRun { std::uint64_t base = 0; std::uint64_t bytes = 0; @@ -631,6 +735,34 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } +ImportRun crossedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + auto last = leasedIndexContaining(lease, begin); + if (last == lease.size()) return {}; + const auto first = last; + while (lease[last]->address + lease[last]->bytes < end) { + if (last + 1 == lease.size() || !continuesRun(*lease[last], *lease[last + 1])) return {}; + ++last; + } + if (first == last) return {}; + 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; +} + +ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto window = importWindow.load(std::memory_order_relaxed); + if (window == 0) return {}; + auto run = crossedRun(lease, begin, end); + if (run.ranges.empty()) return run; + if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run; + if (begin == run.base && end == run.base + run.bytes) return run; + return {}; +} + +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + return !crossedRun(lease, begin, end).ranges.empty(); +} + template bool sameRun(const Entry& entry, const ImportRun& run) { if (run.base != entry.base || run.bytes != entry.bytes || run.ranges.size() != entry.ranges.size()) return false; @@ -653,45 +785,173 @@ bool wouldRetireImport(const HostImports& state, const ImportRun& run) { } bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { - return sameRun(entry, importRun(lease, entry.base)); + 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)); +} + +bool pointerHeld(const HostImports& state, const HostImport& entry) { + return entry.lastUse >= state.buildFloor; +} + +bool evictable(const HostImports& state, const HostImport& entry) { + constexpr std::uint64_t protectedUses = 256; + return !pointerHeld(state, entry) && entry.lastUse + protectedUses < state.useCounter; +} + +std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) { + std::uint64_t freed = 0; + 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 (oldest == state.imports.end()) break; + freed += oldest->second.bytes; + retireImport(context.device, state, oldest, lease); + } + return freed; +} + +std::uint64_t physicalMemoryBytes() { +#ifdef _WIN32 + MEMORYSTATUSEX status{}; + status.dwLength = sizeof(status); + return GlobalMemoryStatusEx(&status) ? status.ullTotalPhys : 0; +#else + const auto pages = sysconf(_SC_PHYS_PAGES); + const auto pageSize = sysconf(_SC_PAGESIZE); + return pages > 0 && pageSize > 0 ? static_cast(pages) * static_cast(pageSize) : 0; +#endif +} + +std::atomic budgetOverride{0}; + +// Pinned imports count against the driver's system memory heap, which imports and type-3 mirrors share; past it +// ordinary host allocations fail. The budget is APS5_HOST_IMPORT_MIB, else the larger of 6 GiB and half the physical +// memory, at most the largest host heap less 2 GiB. +std::uint64_t importBudget(const Context& context) { + 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; + std::uint64_t hostHeap = 0; + for (std::uint32_t index = 0; index < context.memory.memoryHeapCount; ++index) { + if ((context.memory.memoryHeaps[index].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0) hostHeap = std::max(hostHeap, context.memory.memoryHeaps[index].size); + } + const auto fromMemory = std::max(6144ull << 20u, physicalMemoryBytes() / 2); + constexpr std::uint64_t reserve = 2048ull << 20u; + return hostHeap > reserve ? std::min(fromMemory, hostHeap - reserve) : fromMemory; +} + +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 +// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of +// replacing each other's. +ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) { + const auto window = rangeImportWindow.load(std::memory_order_relaxed); + if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run; + const auto& range = *run.ranges.front(); + if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run; + const auto lastCell = range.bytes / window - 1; + const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); }; + auto windowBegin = range.address + cell(begin) * window; + const auto endCell = cell(end - 1); + auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window; + for (bool grown = true; grown;) { + grown = false; + auto it = state.imports.upper_bound(windowBegin); + if (it != state.imports.begin()) --it; + for (; it != state.imports.end() && it->first < windowEnd; ++it) { + const auto importEnd = it->second.base + it->second.bytes; + if (importEnd <= windowBegin || it->second.base < range.address || importEnd > range.address + range.bytes) continue; + if (it->second.base < windowBegin || importEnd > windowEnd) grown = true; + windowBegin = std::min(windowBegin, it->second.base); + windowEnd = std::max(windowEnd, importEnd); + } + } + if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return run; + run.base = windowBegin; + run.bytes = windowEnd - windowBegin; + return run; } const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); bool importsStale(const Context& context, const HostImports& state); +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* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease) { - if (!importsStale(context, state)) { - if (const auto* current = findImport(state, address, address + 1)) return current; +// 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 bytes = 0; + for (const auto& [base, entry] : state.imports) { + if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.bytes; + } + return bytes; +} + +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 bool fresh = !importsStale(context, state); + if (fresh && end != 0) { + if (const auto* current = findImport(state, address, end)) return current; + } else if (!fresh) { + if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current; } auto run = importRun(lease, address); + // 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) { + if (const auto* current = findImport(state, address, run.ranges.empty() ? address + 1 : run.base + run.bytes)) return current; + } + 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); +} + +const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { + if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; + if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; + 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); +} + +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 auto base = run.base; const auto bytes = run.bytes; - if (const auto found = state.imports.find(base); found != state.imports.end()) { - if (sameRun(found->second, run)) return &found->second; - retireImport(context.device, state, found, lease); - } - for (auto it = state.imports.lower_bound(base); it != state.imports.end() && it->first < base + bytes;) { - const auto next = std::next(it); - retireImport(context.device, state, it, lease); - it = next; - } - if (auto it = state.imports.lower_bound(base); it != state.imports.begin() && std::prev(it)->second.base + std::prev(it)->second.bytes > base) retireImport(context.device, state, std::prev(it), lease); + auto& failures = joined ? state.failedJoins : state.failed; const auto alignment = context.hostImportAlignment; - if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; - if (const auto failed = state.failed.find(base); failed != state.failed.end()) { - if (sameRun(failed->second, run)) return nullptr; - state.failed.erase(failed); + 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())}); + return nullptr; } - // Pinned imports count against the driver's system memory budget; past it ordinary host - // allocations fail, so imports stop at APS5_HOST_IMPORT_MIB (default 6 GiB, which covers the - // registered memory of address-based shaders; past it they copy gigabytes per dispatch). - static const std::uint64_t budget = [] { - const char* value = std::getenv("APS5_HOST_IMPORT_MIB"); - return (value ? std::strtoull(value, nullptr, 10) : 6144ull) << 20u; - }(); - const auto live = state.liveBytes; - if (live + bytes > budget) { + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) 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 (sameRun(failed->second, run)) return nullptr; + failures.erase(failed); + } + // The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with + // what may be evicted) before any of them is retired. + 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; + // A join may replace imports this build already looked up: it runs before any region confirms an import + // (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the + // hold (KeepThroughHold). Refusing it there would keep the run copied for good. + const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); }; + for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { + if (held(it->second)) return nullptr; + replaced += it->second.bytes; + } + const auto budget = importBudget(context); + const auto live = state.liveBytes - replaced; + if (live + bytes > budget && (!evict || live + bytes - evictableBytes(state, base, base + bytes) > budget)) { // 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); @@ -699,10 +959,16 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s refusedBytes += bytes; 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(live >> 20u), static_cast(budget >> 20u), static_cast(refused), static_cast(refusedBytes >> 20u), static_cast(base), static_cast(bytes)); + 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)); } return nullptr; } + for (auto it = first; it != state.imports.end() && it->first < base + bytes;) { + const auto next = std::next(it); + retireImport(context.device, state, it, lease); + it = next; + } + if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; entry.range = run.ranges.front(); entry.ranges = std::move(run.ranges); @@ -714,7 +980,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s 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) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } const auto protection = info.Protect & 0xffu; @@ -726,7 +992,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s cursor = reinterpret_cast(info.BaseAddress) + info.RegionSize; } if (!writable && readOnly) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } if (!writable) { @@ -739,13 +1005,17 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s 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); + return importAllocation(context, state, address, lease, end, evict); } } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); return nullptr; } #endif @@ -776,7 +1046,7 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); #endif - state.failed.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); + 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 static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; @@ -806,16 +1076,12 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s 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)\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0); + 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" : ""); + entry.joined = joined; + entry.lastUse = ++state.useCounter; return &state.imports.emplace(base, entry).first->second; } -// The lease is in address order (it is built from the registry map), so a range is found by binary search. -const GuestAllocations::Range* leasedRangeAt(const GuestAllocations::Lease& lease, std::uint64_t base) { - const auto found = std::lower_bound(lease.begin(), lease.end(), base, [](const auto& range, std::uint64_t value) { return range->address < value; }); - return found != lease.end() && (*found)->address == base ? found->get() : nullptr; -} - // The readable registered range containing [begin, end), or null. const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { const auto found = std::upper_bound(lease.begin(), lease.end(), begin, [](std::uint64_t value, const auto& range) { return value < range->address; }); @@ -840,6 +1106,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc } state.liveBytes = 0; state.failed.clear(); + state.failedJoins.clear(); state.device = context.device; state.destroyBuffer = context.Function("vkDestroyBuffer"); state.freeMemory = context.Function("vkFreeMemory"); @@ -852,19 +1119,19 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc if (generation == state.refreshedGeneration) return; state.refreshedGeneration = generation; for (auto it = state.imports.begin(); it != state.imports.end();) { - const auto* range = leasedRangeAt(lease, it->first); - if (range != nullptr && sameRange(it->second, lease)) { + if (sameRange(it->second, lease)) { if (it->second.unwatched) GuestMemory::Unwatch(it->first, it->second.bytes); ++it; continue; } static const bool trace = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; - if (trace && range != nullptr && range->bytes == it->second.bytes) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); + if (trace && leasedRangeOwner(lease, it->first, it->first + it->second.bytes) != nullptr) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx retired: the range was mapped again\n", static_cast(it->first), static_cast(it->second.bytes)); const auto next = std::next(it); retireImport(context.device, state, it, lease); it = next; } for (auto it = state.failed.begin(); it != state.failed.end();) it = sameRun(it->second, importRun(lease, it->first)) ? 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); } const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { @@ -872,7 +1139,15 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6 if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - return begin >= entry.base && end <= entry.base + entry.bytes ? &entry : nullptr; + if (begin < entry.base || end > entry.base + entry.bytes) return nullptr; + entry.lastUse = ++state.useCounter; + return &entry; +} + +const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { + if (!runCovers(lease, begin, end)) return findImport(state, begin, end); + const auto* entry = importAllocation(context, state, begin, lease, end); + return entry != nullptr && findImport(state, begin, end) == entry ? entry : nullptr; } // Whether the imports need reconciling before a lookup: the registry changed since the last walk. @@ -1785,6 +2060,10 @@ MirrorStats MirrorCounters() { } void ClearImageMirrors(VkDevice device) { + auto space = Spaces().current.load(); + if (space != nullptr && space->device == device) { + Spaces().current.compare_exchange_strong(space, std::shared_ptr{}); + } auto& state = Mirrors(); std::map> entries; { @@ -1795,7 +2074,6 @@ void ClearImageMirrors(VkDevice device) { state.heapBytes = 0; state.device = VK_NULL_HANDLE; } - Spaces().current.store(nullptr); } void ClearHostImports(VkDevice device) { @@ -1813,6 +2091,7 @@ void ClearHostImports(VkDevice device) { state.imports.clear(); state.liveBytes = 0; state.failed.clear(); + state.failedJoins.clear(); state.device = VK_NULL_HANDLE; state.refreshedGeneration = 0; ++state.epoch; @@ -2001,6 +2280,20 @@ void SetHostImportWindow(std::uint64_t bytes) { importWindow.store(bytes, std::memory_order_relaxed); } +void SetHostImportRangeWindow(std::uint64_t bytes) { + rangeImportWindow.store(bytes, std::memory_order_relaxed); +} + +void SetHostImportBudget(std::uint64_t bytes) { + budgetOverride.store(bytes, std::memory_order_relaxed); +} + +std::uint64_t HostImportLiveBytes() { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return state.liveBytes; +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); @@ -2008,13 +2301,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s std::lock_guard lock(state.mutex); // A hit is only valid while the registry has not changed since the imports were reconciled. if (!importsStale(context, state)) { - if (const auto* entry = findImport(state, address, address + bytes)) return entry; + if (const auto* entry = findImport(state, address, address + bytes)) { + entry->lastUse = ++state.useCounter; + return entry; + } } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - if (!runCovers(lease, address, address + bytes)) return nullptr; - const auto* entry = importAllocation(context, state, address, lease); - return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr; + return importAccess(context, state, address, address + bytes, lease); } bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { @@ -2036,8 +2330,12 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& const auto range = mapped.lock(); if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue; try { - if (!GuestMemory::GpuMutex().HeldByThisThread() && wouldRetireImport(state, importRun(current, range->address))) return false; - if (importAllocation(context, state, range->address, current) == nullptr && GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation) state.failed.erase(importRun(current, range->address).base); + if (!GuestMemory::GpuMutex().HeldByThisThread()) { + const auto run = importRun(current, range->address); + 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); } 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()); } @@ -2137,6 +2435,13 @@ void GuestBufferMemory::AcquireRegistered() { else if (current->generation != generation) spaces.rebuiltGeneration.fetch_add(1, std::memory_order_relaxed); else if (current->importsEpoch != state.epoch) spaces.rebuiltEpoch.fetch_add(1, std::memory_order_relaxed); else hit = true; + 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; + } + } } if (hit) { for (auto* mirror : current->sparseMirrors) { @@ -2175,6 +2480,8 @@ void GuestBufferMemory::AcquireRegistered() { auto& state = Imports(); std::lock_guard lock(state.mutex); if (importable) refreshImports(context, state, lease); + if (importFloor == 0) importFloor = state.useCounter + 1; + state.buildFloor = importFloor; // Taken before the pass: the imports it takes are the registry's ones (just reconciled, so // none is retired by a make below), and a mirror's preparation may sync the recorder, whose // completions can retire an import already taken; that moves the epoch, and Upload then @@ -2196,7 +2503,7 @@ void GuestBufferMemory::AcquireRegistered() { if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); - entry = importAllocation(context, state, range->address, lease); + entry = importAllocation(context, state, range->address, lease, 0, false); } lap(timing.importsUs, at); if (entry != nullptr) { @@ -2395,7 +2702,21 @@ const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address) return own; } -void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { +namespace { + +template +bool mirrorOverlaps(const Regions& base, std::uint64_t begin, std::uint64_t end) { + auto found = std::upper_bound(base.begin(), base.end(), begin, [](std::uint64_t value, const auto& region) { return value < region.begin; }); + if (found != base.begin()) --found; + for (; found != base.end() && found->begin < end; ++found) { + if (found->end > begin && found->mirror != nullptr) return true; + } + return false; +} + +} + +void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic, bool swept, bool readOnly) { validate(address, bytes); const auto begin = address & ~std::uint64_t{3}; const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3}; @@ -2408,6 +2729,10 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b } return; case BaseOverlap::Partial: + if (readOnly && !mirrorOverlaps(space->base, begin, end) && !WritesOverlap(begin, static_cast(end - begin))) { + readsOverBase.emplace_back(begin, end); + break; + } Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); dissolveSpace(false); break; @@ -2473,7 +2798,11 @@ bool GuestBufferMemory::bindSparseMirror(Region& region, const GuestAllocations: } void GuestBufferMemory::AddWritable(std::uint64_t address, std::size_t bytes, bool atomic, bool swept) { - addDescriptorRegion(address, bytes, atomic, swept); + if (space != nullptr && std::any_of(readsOverBase.begin(), readsOverBase.end(), [&](const auto& range) { return address < range.second && range.first < address + bytes; })) { + Spaces().dissolvedOverlap.fetch_add(1, std::memory_order_relaxed); + dissolveSpace(false); + } + addDescriptorRegion(address, bytes, atomic, swept, false); writes.emplace_back(address, address + bytes); } @@ -2481,7 +2810,7 @@ void GuestBufferMemory::AddReadable(std::uint64_t address, std::size_t bytes) { // Not in `writes`: no reference copy (copyRegion), no write-back, no pending-write note, no // direct-write mark, and UploadPrepare copies it without the device lock. A written descriptor // overlapping the range still covers it through its own Writes() entry. - addDescriptorRegion(address, bytes, false, false); + addDescriptorRegion(address, bytes, false, false, true); } void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) { @@ -2845,6 +3174,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); + if (importFloor == 0) importFloor = state.useCounter + 1; const bool stale = importsStale(context, state); for (auto& region : regions) { if (region.mirror != nullptr) continue; @@ -2927,6 +3257,11 @@ void GuestBufferMemory::UploadFinish(bool addressable) { } uploaded = true; for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); + if (context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + state.buildFloor = importFloor != 0 ? importFloor : state.useCounter + 1; + } // Registered ranges to import from. Address-based shaders hold their lease until write-back; // descriptor-only uploads acquire one only when the registry changed or an import must be made // (a lease copies every registered range's shared_ptr under the registry lock), so the guest can @@ -2956,6 +3291,17 @@ void GuestBufferMemory::UploadFinish(bool addressable) { ++Mirrors().subranges; } bool importsRefreshed = false; + if (context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) refreshImports(context, state, importRanges()); + importsRefreshed = true; + for (const auto& region : regions) { + if (!region.pending || region.mirror != nullptr || region.direct != nullptr || sparseHeapCandidate(region)) continue; + if (findImport(state, region.begin, region.end) != nullptr || runCovers(importRanges(), region.begin, region.end)) continue; + static_cast(importJoined(context, state, region.begin, region.end, importRanges())); + } + } if (space != nullptr && context.hostImportAlignment != 0) { // The space's regions were confirmed against the imports when it was built; one check that // none was retired since (a mirror refresh or a flush between the stages can reconcile @@ -3013,7 +3359,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { region.direct = nullptr; if (entry == nullptr) entry = findImport(state, region.begin, region.end); if (entry == nullptr) { - if (runCovers(importRanges(), region.begin, region.end)) entry = importAllocation(context, state, region.begin, importRanges()); + entry = importAccess(context, state, region.begin, region.end, importRanges()); } // A staged region (see stagingEligible) is copied out of the import even when aligned; // without a recorder to record the copies it binds in place like any other. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 8e854983e7..55ecd30e30 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -235,4 +235,11 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } +bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view) { + if (view.dimension == surface.dimension && view.depthOrLastArray == surface.depthOrLastArray) return true; + if (surface.dimension != TextureDimension::k2DArray) return false; + if (view.dimension == TextureDimension::k2D) return view.baseArray == 0; + return view.dimension == TextureDimension::k2DArray && view.depthOrLastArray <= surface.depthOrLastArray; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index a688a413bb..792b1395a0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include #include #include @@ -153,6 +154,7 @@ public: } Context context; + std::shared_ptr> active = std::make_shared>(true); ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -175,6 +177,7 @@ struct GuardedRange { std::uint64_t address; std::uint64_t bytes; std::uint32_t samples; + std::shared_ptr> active; }; class PrimedSharedMutex : public std::shared_mutex { @@ -204,7 +207,17 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } -void guard(const Context& context, const ColorTarget& target) { +void retireUnmapped(std::uintptr_t address, std::size_t bytes) { + std::unique_lock lock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { + if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; + range.active->store(false, std::memory_order_release); + return true; + }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); +} + +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -216,10 +229,13 @@ void guard(const Context& context, const ColorTarget& target) { bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; } std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples}); + guarded().push_back({context.device, target.address, bytes, target.samples, active}); anyGuarded().store(true, std::memory_order_release); static std::once_flag installed; - std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); + std::call_once(installed, [] { + GuestMemory::SetReadGuard(&guardReads); + }); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&retireUnmapped); } std::mutex& targetsMutex() { @@ -234,14 +250,15 @@ std::vector>& targets() { 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; target->cmask.CheckAddress(use.cmaskAddress); target->ApplyFastClear(use); return *target; } targets().push_back(std::make_unique(context, use)); - guard(context, use); auto& created = *targets().back(); + guard(context, use, created.active); if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; @@ -385,9 +402,19 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, destination.MarkDirty(); } +bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + if (bytes == 0) return false; + std::lock_guard lock(targetsMutex()); + return std::any_of(targets().begin(), targets().end(), [&](const auto& target) { + VkClearColorValue clear{}; + return target->active->load(std::memory_order_acquire) && target->target.address == address && MultisampledColorBytes(target->target) == bytes && MultisampledFillClearColor(target->target, pattern, clear); + }); +} + void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; target->colorFill.NoteFill(address, bytes, pattern); const auto before = target->cmask.Keys(); target->cmask.NoteFill(address, bytes, pattern); @@ -403,6 +430,7 @@ void ClearMultisampleTargets(VkDevice device) { std::unique_lock guardLock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); anyGuarded().store(!guarded().empty(), std::memory_order_release); + if (guarded().empty()) GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); } void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 6eeefefa2a..e010e98c22 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -499,13 +499,18 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput return; } if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, false, _modules); - else Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + else { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines"); + } if (alternateAlpha) { blend.pAttachments = secondChannels.data(); + auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines"); } if (state.depthBiasPerFace) { raster.cullMode = VK_CULL_MODE_FRONT_BIT; + auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines"); } for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); @@ -828,6 +833,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.partitioning); append(key, tessellation.outputTopology); append(key, tessellation.patchesPerGroup); + append(key, tessellation.primitiveGroupSize); + append(key, tessellation.breakAtInstance); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index b3f1de78cb..ec51c5a20b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -106,11 +106,15 @@ void PipelineCache::load(std::vector& initialData) { void PipelineCache::save(bool final) { const auto getData = context.Function("vkGetPipelineCacheData"); std::size_t size = 0; - if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; - std::vector file(sizeof(FileHeader) + size); - const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); - if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; - file.resize(sizeof(FileHeader) + size); + std::vector file; + { + auto synchronizationLock = synchronization.LockForSnapshot(); + if (getData(context.device, cache, &size, nullptr) != VK_SUCCESS || size == 0 || size == savedBytes) return; + file.resize(sizeof(FileHeader) + size); + const auto result = getData(context.device, cache, &size, file.data() + sizeof(FileHeader)); + if (result != VK_SUCCESS && result != VK_INCOMPLETE) return; + file.resize(sizeof(FileHeader) + size); + } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; std::memcpy(file.data(), &header, sizeof(header)); const auto started = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index bfdb538df6..bd2a12ed32 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -205,7 +205,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } VkPipeline handle = VK_NULL_HANDLE; if (partsOnly) lock.unlock(); - const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); + } if (partsOnly) lock.lock(); Check(created, "vkCreateGraphicsPipelines library"); if (const auto raced = cache.find(key); raced != cache.end()) { @@ -234,7 +238,10 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; - Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); + } ++device.counters.linked; if (optimize && optimized != nullptr) { auto slot = std::make_shared(); @@ -243,8 +250,9 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip *optimized = slot; const auto createPipelines = context.Function("vkCreateGraphicsPipelines"); const auto cache = context.pipelineCache; + const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] { + enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); @@ -253,7 +261,11 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip create.flags = VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT; create.layout = libraryLayout; VkPipeline result = VK_NULL_HANDLE; - if (createPipelines(slot->device, cache, 1, &create, nullptr, &result) != VK_SUCCESS) return; + const auto pipelineCreationResult = [&] { + auto pipelineCacheLock = cacheSynchronization != nullptr ? cacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; + return createPipelines(slot->device, cache, 1, &create, nullptr, &result); + }(); + if (pipelineCreationResult != VK_SUCCESS) return; slot->handle.store(result); if (slot->released.load()) { if (const auto handle = slot->handle.exchange(VK_NULL_HANDLE)) slot->destroy(slot->device, handle, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 192d1e9c3c..f3acc6488e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1998,7 +1998,10 @@ bool Recorder::gpuSampleCounter() { pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = sampleLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &samplePipeline), "vkCreateComputePipelines sample counter"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; @@ -2009,7 +2012,10 @@ bool Recorder::gpuSampleCounter() { Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dumpPipeline), "vkCreateComputePipelines sample dumps"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); module = VK_NULL_HANDLE; } catch (const std::exception& error) { @@ -2037,7 +2043,7 @@ std::vector Recorder::QueuedWriteGroups(std::span> computed; for (std::size_t i = 0; i < writes.size(); ++i) { const auto& write = writes[i]; - if (!write.computed && std::any_of(computed.begin(), computed.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; })) { + if (!write.computed && !computed.empty()) { ends.push_back(i); computed.clear(); } @@ -2286,7 +2292,10 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec pipelineInfo.stage.module = module; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = meshArgumentLayout; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &meshArgumentPipeline), "vkCreateComputePipelines mesh arguments"); + } context.Function("vkDestroyShaderModule")(context.device, module, nullptr); meshArgumentState = 1; } catch (const std::exception& error) { @@ -2654,7 +2663,8 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte if (bytes == 0) return; if (&batch == open.get()) { if (!noteWrite(address, bytes, ownLabel)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); return; } @@ -2666,7 +2676,10 @@ void Recorder::noteWriteOn(Batch& batch, std::uint64_t address, std::size_t byte batch.writeValues.push_back(-1); if (!ownLabel) markOverwritten(address, address + bytes); if (activeRecorder == this) writeGeneration.fetch_add(1, std::memory_order_release); - if (!SnapshotCovers(address, address + bytes)) publishPendingWrites(); + if (!SnapshotCovers(address, address + bytes)) { + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); + } std::atomic_thread_fence(std::memory_order_seq_cst); } @@ -2679,7 +2692,8 @@ void Recorder::markOverwritten(std::uint64_t address, std::uint64_t end) { void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { if (!noteWrite(address, bytes)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); // The note precedes this thread's vkQueueSubmit and the label another queue polls for; the // fence makes that order hold without relying on x86 store ordering. std::atomic_thread_fence(std::memory_order_seq_cst); @@ -2687,7 +2701,8 @@ void Recorder::NotePendingWrite(std::uint64_t address, std::size_t bytes) { void Recorder::NotePendingFill(std::uint64_t address, std::size_t bytes, std::uint8_t value) { if (!noteWrite(address, bytes, false, value)) return; - publishPendingWrites(); + const std::array ranges{std::pair{address, address + bytes}}; + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); } @@ -2697,26 +2712,55 @@ void Recorder::NotePendingWrites(std::span begin && noteWrite(begin, static_cast(end - begin))) publish = true; } if (!publish) return; - publishPendingWrites(); + publishPendingWrites(ranges); std::atomic_thread_fence(std::memory_order_seq_cst); } -void Recorder::publishPendingWrites() const { - // Only the active recorder owns the snapshot: one torn down after its successor was activated, - // or one a worker still dispatches into after device.reset(), must not publish its ranges. - if (activeRecorder != this) return; - const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto merged = std::make_shared(); - if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); - for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); - std::sort(merged->begin(), merged->end()); - std::size_t out = 0; - for (const auto& [begin, end] : *merged) { - if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); - else (*merged)[out++] = {begin, end}; +void Recorder::publishPendingWrites(std::span> additions) const { + if (activeRecorder != this) { + publishedWriteRangesValid = false; + return; } - merged->resize(out); + const auto started = DrawProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + const auto previous = pendingWrites.load(std::memory_order_acquire); + const auto owned = publishedWriteRanges.lock(); + const bool extend = !additions.empty() && previous != nullptr && previous == owned && publishedWriteRangesValid && + std::all_of(additions.begin(), additions.end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRangesValid = false; + auto merged = std::make_shared(); + if (extend && additions.size() == 1) { + auto range = additions.front(); + const auto first = std::partition_point(previous->begin(), previous->end(), [&](const auto& entry) { return entry.second < range.first; }); + auto last = first; + while (last != previous->end() && last->first <= range.second) { + range.first = std::min(range.first, last->first); + range.second = std::max(range.second, last->second); + ++last; + } + merged->reserve(previous->size() - static_cast(last - first) + 1); + merged->insert(merged->end(), previous->begin(), first); + merged->push_back(range); + merged->insert(merged->end(), last, previous->end()); + } else { + if (extend) { + merged->reserve(previous->size() + additions.size()); + merged->insert(merged->end(), previous->begin(), previous->end()); + merged->insert(merged->end(), additions.begin(), additions.end()); + } else { + if (open != nullptr) merged->insert(merged->end(), open->writes.begin(), open->writes.end()); + for (const auto& batch : inFlight) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + for (const auto* batch : finishing) merged->insert(merged->end(), batch->writes.begin(), batch->writes.end()); + } + std::sort(merged->begin(), merged->end()); + std::size_t out = 0; + for (const auto& [begin, end] : *merged) { + if (out != 0 && begin <= (*merged)[out - 1].second) (*merged)[out - 1].second = std::max((*merged)[out - 1].second, end); + else (*merged)[out++] = {begin, end}; + } + merged->resize(out); + } + publishedWriteRangesValid = std::all_of(merged->begin(), merged->end(), [](const auto& range) { return range.second > range.first; }); + publishedWriteRanges = merged; pendingWrites.store(std::move(merged), std::memory_order_release); publishGeneration.fetch_add(1, std::memory_order_release); ++snapshotRebuilds; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index c21f1d06ad..e675a2c212 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -81,17 +81,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag allocationBytes = allocation.allocationSize; // The CPU reads most of these buffers back (write-back, diffs), which is very slow from // write-combined memory, so the default host properties prefer cached host memory. - constexpr VkMemoryPropertyFlags hostDefault = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - if (properties == hostDefault) { - try { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); - } catch (const std::runtime_error&) { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, hostDefault); - } - } else { - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, properties); - } - if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize)) { + const bool useSlab = (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0 && BufferPool::SlabEligible(capacity, requirements.alignment, requirements.size, context.limits.nonCoherentAtomSize); + allocation.memoryTypeIndex = context.BufferMemoryType(requirements.memoryTypeBits, properties, useSlab ? BufferPool::SlabBlockBytes(capacity) : allocation.allocationSize); + if (useSlab) { const auto slot = cache->TakeSlot(context, allocation.memoryTypeIndex, capacity, addressable); memory = slot.memory; offset = slot.offset; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 3bf2f95565..214fcaf05f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -113,10 +113,12 @@ namespace AgcDriver::Graphics { return requiresNonSeamlessCube; } - void RequireDegammaFormat(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + bool ForcesDegammaOn(std::uint32_t guestFormat, std::uint32_t samplerMask, std::span> samplers) { + if (IsSrgbTextureFormat(guestFormat)) return false; for (std::uint32_t element = 0; element < 32u && element < samplers.size(); ++element) { - if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma() && !IsSrgbTextureFormat(guestFormat)) Require(false, "a texture of guest format " + std::to_string(guestFormat) + ", which is not sRGB, is sampled through a sampler that forces sRGB decoding, which is not implemented"); + if (((samplerMask >> element) & 1u) != 0u && samplers[element]->ForcesDegamma()) return true; } + return false; } void RequireBorderSwizzle(std::uint32_t bcSwizzle, std::uint32_t samplerMask, std::span> samplers) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 552145a607..4926b3dd01 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" @@ -111,6 +112,7 @@ struct CachedTexture { std::uint64_t sourceVersion = 0; std::uint64_t accounted = 0; std::uint64_t present = 0; + std::optional>> committed; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -554,7 +556,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; auto source = StorageTexture::FindPending(address, guestBytes); - if (source != nullptr && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); + if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -626,11 +629,25 @@ std::shared_ptr cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && blockCopy) { + auto texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); return std::make_shared(context, *context.detiler, resource, components, snapshot, depthCompare); } + auto snapshotGeneration = generation; + std::optional>> committed; + if (source == nullptr && generation == 0) { + std::vector> ranges; + if (const auto partial = GuestMemory::CollectCommittedWrites(address, bytes, ranges); partial != 0) { + snapshotGeneration = partial; + committed = std::move(ranges); + } + } auto& cache = Textures(); const auto key = MakeTextureKey(context.device, words, components, depthCompare); std::lock_guard lock(cache.mutex); @@ -659,8 +676,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes); }; - if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { - it->generation = generation; + 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); + }; + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || unchanged() || equalsCommitted()) { + it->generation = snapshotGeneration; + it->committed = std::move(committed); touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); reportTextureCounters(); @@ -671,13 +693,17 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, generation}; + CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); if (depthCompare) { entry.texture = std::make_shared(context, source, resource, components, Texture::DepthCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (blockCopy) { + entry.texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared(context, source, resource, components); } @@ -1617,7 +1643,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); - else images.push_back({VK_NULL_HANDLE, textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); + else images.push_back({VK_NULL_HANDLE, textureSrgb[index] ? textures[index]->SrgbView(textureFirstLayer[index], textureBaseLevel[index]) : textureBaseLevel[index] ? textures[index]->BaseLevelView() : textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); break; @@ -1805,7 +1831,18 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB std::vector ShaderResources::ContentKey(const CompiledShader& shader, bool dataWords, bool movableBuffers) { Require(shader.program != nullptr, "missing compiled shader"); std::vector key; - key.reserve(8u + shader.program->bindings.size() * 12u); + std::size_t words = 5; + const auto addWords = [&](std::size_t count) { + if (count > key.max_size() - words) throw std::length_error("resource content key exceeds vector limits"); + words += count; + }; + for (const auto& binding : shader.program->bindings) { + addWords(18); + addWords(binding.imageSamplers.size()); + if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size()); + for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); + } + key.reserve(words); VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); return key; } @@ -3221,7 +3258,7 @@ bool ShaderResources::precollectImages() { } std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) { - if (record.texture == nullptr || record.texture->RefreshedPerUse()) return nullptr; + if (record.texture == nullptr || record.texture->RefreshedPerUse() || record.texture->IsDepthCopy()) return nullptr; struct Outcome { bool profile; std::chrono::steady_clock::time_point start; @@ -3268,7 +3305,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) auto& cache = Textures(); std::lock_guard lock(cache.mutex); const auto it = findSampledEntry(cache, context.device, record.words, record.components); - if (it == cache.entries.end() || it->texture != record.texture) return nullptr; + if (it == cache.entries.end() || it->texture != record.texture || it->committed.has_value()) return nullptr; if (it->source == nullptr) it->generation = record.generation; touchTexture(cache, it); logLookup({record.texture.get(), record.resource, record.guestBytes, keys, record.source != nullptr ? 0 : record.generation, record.source.get()}); @@ -3308,7 +3345,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind continue; } const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); - RequireDegammaFormat(resource.format, binding.imageSamplers[element], shaderSamplers); + 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)) + ")"); @@ -3335,6 +3372,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers); RequireBorderSwizzle(resource.bcSwizzle, binding.imageSamplers[element], shaderSamplers); + if (forcedSrgb && !texture->HasSrgbView()) Require(false, "a texture of guest format " + std::to_string(resource.format) + ", which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding, which is not implemented"); if (!shaderSamplers.empty()) { const bool singleLevel = DescriptorSingleLevel(words); const auto firstSampler = static_cast(shaderSamplers.data() - samplers.data()); @@ -3352,6 +3390,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind textures.push_back(std::move(texture)); textureFirstLayer.push_back(firstLayer); textureBaseLevel.push_back(baseLevel); + textureSrgb.push_back(forcedSrgb); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(textures.size() - 1); } @@ -3546,7 +3585,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); - const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); + const auto maxSnapshotBytes = maxDrawSnapshotBytes; std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 25148b44ec..0b410d2dd0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -115,7 +115,7 @@ struct Module { return; } // One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance. - if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation)) || (value == spv::BuiltInCullDistance && vertex))) { + if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { const auto signature = Signature(type); auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances; Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output"); @@ -250,10 +250,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; - if ((vertex || evaluation) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { + if ((vertex || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { char word[16]; std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl); - Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : vertex && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); + Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : (vertex || mesh) && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); } // Enabled unconditionally or by the device setup in VulkanDevice. @@ -262,12 +262,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || - ((vertex || evaluation) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || capability == spv::CapabilityStorageImageMultisample || - (vertex && shaderCullDistance && capability == spv::CapabilityCullDistance) || + ((vertex || mesh) && shaderCullDistance && capability == spv::CapabilityCullDistance) || capability == spv::CapabilityStorageImageWriteWithoutFormat || capability == spv::CapabilityStorageImageReadWithoutFormat || capability == spv::CapabilityInt64 || @@ -445,7 +445,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const bool quads = state.rectList || state.stages.tessellation->domain == 2u; const bool counterClockwise = !state.rectList && state.stages.tessellation->outputTopology == 3u; mode(quads ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); - mode(state.rectList ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); + mode(state.rectList || state.stages.tessellation->partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd, {}); mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8a07f6bc87..955504eccb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -101,6 +101,28 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt // DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict. constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u); constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u); + +std::uint32_t inactiveVertexPointSize(const QueueState& queue) { + const auto stages = find(queue.context, 0x2d5); + const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig); + const auto raster = find(queue.context, 0x205); + if (stages == queue.context.end() || primitive == queue.userConfig.end() || raster == queue.context.end()) return 0; + if ((stages->second & 0x24u) != 0) return 0; + if ((raster->second & 8u) != 0 && (raster->second & 0x7e0u) != 0x240u) return 0; + switch (primitive->second) { + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 17: + return 1u << 16u; + default: + return 0; + } +} + constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. @@ -108,6 +130,27 @@ constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u | 0x0001 constexpr std::uint32_t PixelStageRunsMask = 0x00020747u; constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u; constexpr std::uint32_t AlphaToCoverageUndithered = 0x0000aa01u; +constexpr std::uint32_t AlphaToCoverageDithered = 0x00018701u; + +bool alphaToCoverageDisabled(const Registers& cx) { + const auto control = find(cx, 0x203); + return control != cx.end() && (control->second & 0x800u) != 0; +} + +bool ditheredAlphaSamplesSupported(const Registers& cx) { + const auto config = find(cx, 0x2f8); + const auto eqaa = find(cx, 0x201); + return config != cx.end() && (config->second & 0x700007u) == 0x300003u && eqaa != cx.end() && (eqaa->second & 0x207777u) == 0x3303u; +} + +bool ditheredAlphaLocationsSupported(const Registers& cx) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + const auto first = find(cx, 0x2fe + pixel * 4u); + const auto second = find(cx, 0x2ff + pixel * 4u); + if (first == cx.end() || second == cx.end() || first->second != 0xbd153fd1u || second->second != 0x9773f95bu) return false; + } + return true; +} constexpr std::uint32_t ScanModeMask = ~0x22u; constexpr std::uint32_t ScanControlMask = ~0x76023fffu; constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu; @@ -336,7 +379,7 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { const auto offset = 0x2feu + pixel * 4u; if (const auto locations = word(cx, offset); locations && (*locations & 0xffu) != 0) return zeroMessage(offset, *locations, "MSAA_ENABLE on a single-sample target whose sample is off the pixel center"); } - if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*alphaToMask == AlphaToCoverageDithered && alphaToCoverageDisabled(cx))) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; const auto stages = word(cx, 0x2d5); @@ -624,20 +667,23 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate((value & ~0x0207ed0du) == 0 && (value & 0x02002000u) == 0x02002000u && (value & 3u) == 1u && ((value >> 3u) & 3u) == 1u, "unsupported tessellation routing"); const auto config = read(queue.context, 0x2d6); const auto parameters = read(queue.context, 0x2db); - ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu}; + const auto primitiveGroup = find(queue.userConfig, 0x25b, RegisterBank::UserConfig); + ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu, primitiveGroup == queue.userConfig.end() ? 0u : primitiveGroup->second & 0x1ffu, primitiveGroup != queue.userConfig.end() && (primitiveGroup->second & 0x40000u) != 0u}; validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && tess.partitioning == 2 && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with fractional-odd partitioning and triangle output are supported by the reference path"); + validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer or fractional-odd partitioning and triangle output are supported by the reference path"); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); result.tessellation = tess; } else { - validate((value & ~0x0047ec30u) == 0, "unsupported geometry routing, fast launch or wave-ID state"); + const bool fastLaunch = ((value >> 19u) & 3u) == 1u; + validate((value & ~(fastLaunch ? 0x004fec30u : 0x0047ec30u)) == 0, "unsupported geometry routing, fast launch or wave-ID state"); const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig); const auto vertices = (group >> 9u) & 0x1ffu; const auto primitives = group & 0x1ffu; + validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); const auto maxVertices = read(queue.context, 0x1ff); const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly"); @@ -648,11 +694,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit}); validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); - validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); + validate(vertexWorkgroup || fastLaunch || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, primitives * (verticesPerPrimitive - 2u), ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; + const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u; + validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); + const auto maxPrimitives = primitives * amplification; + const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; } return result; } @@ -707,7 +756,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } APS5_LOG_OUT_DEBUG("Topology=%u", static_cast(result.topology)); result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; - zero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); result.samples = decodeSamples(cx); @@ -739,8 +788,15 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); } zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); - result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; - if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); + const auto alpha = read(cx, 0x2dc); + const bool alphaDisabled = alpha == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + result.alphaToCoverage = alpha == AlphaToCoverageUndithered || (!alphaDisabled && alpha == AlphaToCoverageDithered); + result.ditheredAlphaToCoverage = result.alphaToCoverage && alpha == AlphaToCoverageDithered; + if (!result.alphaToCoverage) zero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); + if (result.ditheredAlphaToCoverage) { + Require(ditheredAlphaSamplesSupported(cx), "dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"); + Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); + } Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); @@ -893,11 +949,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t component = 0; component < 4; ++component) { if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; } - state.blendEnable = (blend >> 30u) & 1u; + state.blendEnable = ((blend >> 30u) & 1u) != 0 && (read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0; if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented"); Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported"); if (state.blendEnable) { - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); state.srcColorBlendFactor = blendFactor(blend & 0x1fu); state.dstColorBlendFactor = blendFactor((blend >> 8u) & 0x1fu); state.colorBlendOp = blendOp((blend >> 5u) & 7u); @@ -959,10 +1014,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } if (result.alphaToCoverage) { const auto mrt0 = std::find_if(result.colors.begin(), result.colors.end(), [](const ColorTarget& color) { return color.exportIndex == 0; }); - Require(mrt0 != result.colors.end(), "alpha-to-coverage without a written MRT0 color target is unsupported"); - const auto format = exportFormat & 0xfu; - Require(format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); - Require(mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None, "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); + const auto format = read(cx, 0x1c5) & 0xfu; + Require(format == 3u || format == 4u || format == 5u || format == 6u || format == 9u, "alpha-to-coverage needs an MRT0 export format with a floating-point alpha"); + Require(!result.ditheredAlphaToCoverage || format == 4u || format == 9u, "dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"); + Require(mrt0 == result.colors.end() || (mrt0->componentMapping == 0xe4u && mrt0->packing == ShaderRecompiler::ColorExportPacking::None), "alpha-to-coverage into a component-swapped or packed MRT0 target is unsupported"); } APS5_LOG_OUT_DEBUG("DecodeState done colorTarget=%u render=%ux%u topology=%u", result.hasColorTarget ? 1u : 0u, result.renderExtent.width, result.renderExtent.height, static_cast(result.topology)); return result; @@ -1142,7 +1197,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } - if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) { + if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) { Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); @@ -1190,7 +1245,18 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (mode != 2u && mode != 6u && !resolve) return std::nullopt; ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); - Require(depthPlanesAbsent(cx) || ((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0), "CB metadata pass with depth or stencil work"); + const auto value = [&](std::uint32_t offset) { + const auto found = find(cx, offset); + return found == cx.end() ? 0u : found->second; + }; + const bool planes = !depthPlanesAbsent(cx); + const auto depthControl = planes ? read(cx, 0x200) : 0u; + const bool depthWork = (depthControl & 0xfu) != 0 && ((depthControl & 8u) != 0 || !depthPassThrough(depthControl)); + if (depthWork || (planes && (read(cx, 0x0) & 0xfu) != 0)) { + char text[192]; + std::snprintf(text, sizeof(text), "CB metadata pass with depth or stencil work (DB_DEPTH_CONTROL 0x%08x, DB_RENDER_CONTROL 0x%08x, DB_Z_INFO 0x%08x, DB_STENCIL_INFO 0x%08x)", value(0x200), value(0x0), value(0x010), value(0x011)); + throw std::runtime_error(text); + } const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); @@ -1259,7 +1325,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } - if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported"); if (value(cx, 0x200, word)) { const auto renderControl = find(cx, 0x000); @@ -1282,8 +1348,21 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t zFormat = 0; static_cast(value(cx, 0x1c4, zFormat)); if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; - if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { - if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + if (value(cx, 0x2dc, word)) { + const bool alphaDisabled = word == AlphaToCoverageDithered && alphaToCoverageDisabled(cx); + if ((word != AlphaToCoverageUndithered && word != AlphaToCoverageDithered) || alphaDisabled) { + if (auto reason = nonzero(cx, 0x2dc, alphaDisabled ? AlphaToCoverageMask & ~1u : AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; + } + if (!alphaDisabled && word == AlphaToCoverageDithered && !ditheredAlphaSamplesSupported(cx)) return "AGC graphics: dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"; + if (!alphaDisabled && word == AlphaToCoverageDithered) { + if (!ditheredAlphaLocationsSupported(cx)) return "AGC graphics: dithered alpha-to-coverage with custom sample locations is unsupported"; + std::uint32_t format = 0; + if (value(cx, 0x1c5, format)) { + format &= 0xfu; + if (format != 3u && format != 4u && format != 5u && format != 6u && format != 9u) return "AGC graphics: alpha-to-coverage needs an MRT0 export format with a floating-point alpha"; + if (format != 4u && format != 9u) return "AGC graphics: dithered alpha-to-coverage needs an FP16_ABGR or 32_ABGR export"; + } + } } if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d4e73c7410..0ea0b61619 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -89,6 +90,7 @@ std::atomic pretestSkipped{0}, unregisteredDropped{0}, keyFlipKep std::atomic flushedAfterSkip{0}, flushedAfterSkipSameSite{0}, staleEvicted{0}; std::atomic depthCopyTextures{0}, depthCopyRefreshes{0}; std::atomic refreshesProved{0}; +std::atomic untrackedCompares{0}; struct StorageTraffic { std::mutex mutex; @@ -267,7 +269,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto linearBytes = sliceLinearBytes * arrayLayers; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + 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.format = vkFormat; imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; @@ -446,6 +448,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -475,6 +478,75 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; } +bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + const auto& from = source.Descriptor(); + if (!IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format) || BytesPerElement(from.format) != BytesPerElement(descriptor.format)) return false; + if (descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return false; + if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.dimension != from.dimension || descriptor.tileMode != from.tileMode || descriptor.depthOrLastArray != from.depthOrLastArray || descriptor.mipCount > from.mipCount) return false; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const auto width = std::max(descriptor.width >> level, 1u); + const auto height = std::max(descriptor.height >> level, 1u); + if (width % 4u != 0 || height % 4u != 0 || std::max(from.width >> level, 1u) != width / 4u || std::max(from.height >> level, 1u) != height / 4u) return false; + } + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { + try { + Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture"); + blockFormat = SampledTextureFormat(context, descriptor.format); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = blockFormat; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage block copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "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; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = blockFormat; + viewFormat = blockFormat; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_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 block copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + } catch (...) { + release(); + throw; + } +} + Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { try { Require(source != nullptr && CanDepthCopyFrom(*source, descriptor), "storage image cannot serve a depth copy of the sampled texture"); @@ -537,6 +609,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& bool Texture::RefreshDepthCopy() { if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; if (copiedOnce && storageSource->Version() == copiedVersion) return false; + if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); std::uint64_t linearBytes = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -608,6 +681,51 @@ bool Texture::RefreshDepthCopy() { return true; } +bool Texture::refreshBlockCopy() { + std::vector regions; + for (std::uint32_t level = 0; level < copyLevels; ++level) { + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, copyLayers}; + region.dstSubresource = region.srcSubresource; + region.extent = {std::max(copyWidth >> level, 1u) / 4u, std::max(copyHeight >> level, 1u) / 4u, 1u}; + regions.push_back(region); + } + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + context.Function("vkCmdCopyImage")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + Texture::DepthCopyCounts Texture::TakeDepthCopyCounts() { return {depthCopyTextures.exchange(0, std::memory_order_relaxed), depthCopyRefreshes.exchange(0, std::memory_order_relaxed)}; } @@ -617,7 +735,7 @@ bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResour if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; // Same memory, same layout, same texel size: the GPU copy reinterprets the texels exactly as a // guest read through the sampled descriptor would. - return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && descriptor.dimension == from.dimension && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && descriptor.depthOrLastArray == from.depthOrLastArray && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); + return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && ArrayLayersServeView(from, descriptor) && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); } Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components) : context(context), storageSource(source) { @@ -648,6 +766,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); if (profile) { auto& totals = Profile(); totals.view += timer.lap(); @@ -700,6 +819,44 @@ void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkIma baseLevelInfo = viewInfo; } +void Texture::prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { + const auto format = SrgbTwinFormat(context, viewInfo.format); + if (format == VK_FORMAT_UNDEFINED) return; + viewInfo.pNext = nullptr; + viewInfo.format = format; + srgbInfo = viewInfo; + srgbMinLod = EffectiveMinLod(descriptor); +} + +VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { + Require(srgbInfo.has_value(), "AGC graphics: a texture of view format " + std::to_string(viewFormat) + " has no sRGB view for a sampler that forces sRGB decoding"); + std::lock_guard lock(srgbMutex); + auto& slot = srgbViews[baseLevel ? 2u : firstLayer ? 1u : 0u]; + if (slot != VK_NULL_HANDLE) return slot; + VkImageViewCreateInfo info = *srgbInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + if (baseLevel) { + info.viewType = info.viewType == VK_IMAGE_VIEW_TYPE_1D || info.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.levelCount = 1; + info.subresourceRange.layerCount = 1; + } else { + if (firstLayer) { + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.layerCount = 1; + } + if (srgbMinLod != 0.0f) { + minLod.minLod = srgbMinLod; + minLod.pNext = info.pNext; + info.pNext = &minLod; + } + } + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &slot), "vkCreateImageView sRGB"); + return slot; +} + VkImageView Texture::BaseLevelView() const { Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); std::lock_guard lock(baseLevelMutex); @@ -715,6 +872,10 @@ void Texture::release() noexcept { firstLayerView = VK_NULL_HANDLE; if (baseLevelView) context.Function("vkDestroyImageView")(context.device, baseLevelView, nullptr); baseLevelView = VK_NULL_HANDLE; + for (auto& srgbView : srgbViews) { + if (srgbView) context.Function("vkDestroyImageView")(context.device, srgbView, nullptr); + srgbView = VK_NULL_HANDLE; + } // The image and its memory go with the last holder: this texture, or the batch still uploading it. owned.reset(); image = VK_NULL_HANDLE; @@ -817,7 +978,7 @@ bool IntegerFormat(VkFormat format) { switch (format) { case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_UINT: case VK_FORMAT_B8G8R8A8_SINT: - case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: + case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16_SINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32_UINT: case VK_FORMAT_R32G32B32_SINT: @@ -851,6 +1012,23 @@ bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, Dc return IsDccClear(keys) && StorageFormatAvailable(context, guestFormat) && ClearColorFor(StorageFormatForGuest(context, guestFormat), keys, clear); } +VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { + const auto decoded = [&](ShaderRecompiler::IrBufferFormat srgb) { return (context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(srgb)) != 0u; }; + switch (format) { + case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; + case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; + case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: return VK_FORMAT_BC1_RGBA_SRGB_BLOCK; + case VK_FORMAT_BC2_UNORM_BLOCK: return VK_FORMAT_BC2_SRGB_BLOCK; + case VK_FORMAT_BC3_UNORM_BLOCK: return VK_FORMAT_BC3_SRGB_BLOCK; + case VK_FORMAT_BC7_UNORM_BLOCK: return VK_FORMAT_BC7_SRGB_BLOCK; + default: return VK_FORMAT_UNDEFINED; + } +} + VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { if (format != VK_FORMAT_R8_SRGB) return VK_FORMAT_UNDEFINED; struct Table { @@ -876,6 +1054,11 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { std::shared_ptr StorageTexture::Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; + static const bool unwatchHooked = [] { + GuestMemory::SetUnwatchHook(&StorageTexture::SaveBeforeUnwatch); + return true; + }(); + static_cast(unwatchHooked); auto texture = std::shared_ptr(new StorageTexture(context, detiler, descriptor, mipLevel)); uploadReason = "first"; texture->upload(); @@ -1604,7 +1787,13 @@ bool StorageTexture::Refresh() { cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); - if (compared) cpuBlocks = stampedBlocks; + if (compared) { + for (const auto& [from, unitGeneration] : keptAtUnwatch) { + const auto unit = static_cast((from - descriptor.baseAddress) / trackedLayerBytes); + if (keptAtUnwatchHolds(from, unit)) stampedBlocks[static_cast(from / 65536 - descriptor.baseAddress / 65536)] = GuestMemory::BlockWritten; + } + cpuBlocks = stampedBlocks; + } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; keys = ProvedKeys(); @@ -1851,6 +2040,10 @@ std::uint64_t StorageTexture::RefreshesProved() { return refreshesProved.load(std::memory_order_relaxed); } +std::uint64_t StorageTexture::UntrackedCompares() { + return untrackedCompares.load(std::memory_order_relaxed); +} + bool StorageTexture::ServesKeysAt(std::uint64_t dccAddress) const { const bool locked = GuestMemory::GpuMutex().HeldByThisThread(); const auto readFollowed = [&] { @@ -1886,6 +2079,7 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std } void StorageTexture::captureGuestBytes(const std::vector* layers) { + original.resize(static_cast(guestBytes)); comparedGuestBytes = {}; std::vector selected(trackedLayers, true); if (layers != nullptr) selected = *layers; @@ -1921,12 +2115,16 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, const auto first = address / blockBytes; Require(changed.size() == (end - 1) / blockBytes - first + 1, "texture comparison block count differs"); const auto* recorder = Recorder::Active(); - const std::array stamp{GuestMemory::CollectEpoch(), GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; - const bool cacheable = memoize && stamp[0] != 0 && (stamp[2] & 1u) == 0 && address == descriptor.baseAddress && bytes == guestBytes; + const auto epoch = GuestMemory::CollectEpoch(); + const bool whole = address == descriptor.baseAddress && bytes == guestBytes; + const auto hint = memoize && epoch != 0 && whole ? GuestMemory::UntrackedHint(address, bytes) : 0; + const std::array stamp{hint != 0, hint != 0 ? hint : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetSerial(), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0}; + const bool cacheable = memoize && epoch != 0 && (stamp[3] & 1u) == 0 && whole; if (cacheable && comparedGuestBytes == stamp) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); return true; } + untrackedCompares.fetch_add(1, std::memory_order_relaxed); const auto saved = std::span(original).subspan(static_cast(address - descriptor.baseAddress), bytes); if (GuestMemory::EqualsCommittedUnsynced(address, saved)) { std::fill(changed.begin(), changed.end(), GuestMemory::BlockUnchanged); @@ -1953,7 +2151,6 @@ void StorageTexture::upload(const std::vector* layers) { CaptureTrace::Log("upload image=%llx bytes=%llu generation=%llu reason=%s partial=%d", static_cast(descriptor.baseAddress), static_cast(guestBytes), static_cast(generation), uploadReason, layers != nullptr); const auto elementBytes = BytesPerElement(descriptor.format); const auto linearBytes = sliceLinearBytes * arrayLayers; - original.resize(static_cast(guestBytes)); // A layer selection applies to the direct path alone (the others upload everything); it names // array layers, which the tracked layers are when there are several. if (layers != nullptr && ((!blockUnits && trackedLayers != arrayLayers) || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }))) layers = nullptr; @@ -2157,6 +2354,7 @@ void StorageTexture::upload(const std::vector* layers) { layerOriginal.assign(trackedLayers, true); forgetBorrowed(0, trackedLayers); stampLayers(false); + original.resize(static_cast(guestBytes)); GuestMemory::ReadCommitted(descriptor.baseAddress, original); { Buffer staging(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -2807,6 +3005,21 @@ 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) { + 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)); + if (first >= last) { + MarkDirty(); + return; + } + markLayersPending(first, last - first); +} + bool StorageTexture::anyLayerPending() const { return std::any_of(layerPending.begin(), layerPending.end(), [](bool pending) { return pending; }); } @@ -2818,6 +3031,13 @@ 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)); 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; + for (std::uint32_t layer = first; layer < first + count; ++layer) { + unsaved[layer] = !layerPending[layer] && !layerOriginal[layer] && layerGeneration[layer] == 0; + anyUnsaved = anyUnsaved || unsaved[layer]; + } + if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) captureGuestBytes(&unsaved); for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true; // Results of this image now cover the alias's results borrowed into these units: this image // stores them, the alias no longer has to (its results, unchanged since the borrow, are in @@ -2982,6 +3202,72 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons return true; } +void StorageTexture::SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes) { + std::vector> overlapping; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + const auto take = [&](StorageTexture* texture) { + if (!texture->overlaps(address, bytes)) return; + if (auto alive = texture->weak_from_this().lock()) overlapping.push_back(std::move(alive)); + }; + for (auto* texture : pending.textures) take(texture); + for (auto* texture : pending.flushing) take(texture); + } + if (overlapping.empty()) return; + std::lock_guard gpu(GuestMemory::GpuMutex()); + for (const auto& texture : overlapping) texture->saveBeforeUnwatch(); +} + +void StorageTexture::forgetKeptAtUnwatch(std::uint32_t unit) { + const auto begin = layerBegin(unit); + keptAtUnwatch.erase(keptAtUnwatch.lower_bound(begin), keptAtUnwatch.lower_bound(begin + layerBytes(unit))); +} + +bool StorageTexture::keptAtUnwatchHolds(std::uint64_t from, std::uint32_t unit) const { + const auto found = keptAtUnwatch.find(from); + return found != keptAtUnwatch.end() && layerOriginal[unit] && found->second == layerGeneration[unit]; +} + +void StorageTexture::forgetSavedOver(std::uint64_t address, std::size_t bytes) { + // Only these units' bytes changed under the saved copy. Units outside keep it: results still + // pending there (the flush before the fill or copy stored only the units it overlaps) are + // compared with it at their store when the memory is outside the write watch. + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (layerBegin(unit) < address + bytes && address < layerBegin(unit) + layerBytes(unit)) layerOriginal[unit] = false; + } +} + +void StorageTexture::saveBeforeUnwatch() { + std::vector units(trackedLayers, false); + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) units[unit] = layerPending[unit] && !layerOriginal[unit] && layerGeneration[unit] != 0; + const auto runs = unitRuns(units); + if (runs.empty()) return; + original.resize(static_cast(guestBytes)); + comparedGuestBytes = {}; + for (const auto& [begin, end] : runs) GuestMemory::ReadCommittedUnsynced(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); + for (const auto& [begin, end] : runs) GuestMemory::CollectWritesUncached(descriptor.baseAddress + begin, static_cast(end - begin)); + constexpr std::uint64_t block = 65536; + for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { + if (!units[unit]) continue; + const auto begin = layerBegin(unit); + const auto end = begin + layerBytes(unit); + const auto spanBegin = begin & ~(block - 1); + const auto spanBlocks = static_cast((end - spanBegin + block - 1) / block); + const std::vector generations(spanBlocks, layerGeneration[unit]); + std::vector changedBlocks(spanBlocks); + if (!GuestMemory::ChangedBlocks(begin, static_cast(end - begin), generations, changedBlocks)) continue; + forgetKeptAtUnwatch(unit); + for (auto at = spanBegin; at < end; at += block) { + const auto from = std::max(at, begin); + const auto to = std::min(at + block, end); + const bool edge = from != at || to != at + block; + if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]); + } + layerOriginal[unit] = true; + } +} + bool StorageTexture::PublishShadowsOnly(std::uint64_t address, std::size_t bytes, PublishScope scope, const char* reason) { if (scope == PublishScope::None || !AnyShadowedOverlaps(address, bytes)) return false; std::lock_guard gpu(GuestMemory::GpuMutex()); @@ -3063,6 +3349,26 @@ std::shared_ptr StorageTexture::FindPending(std::uint64_t addres return containing != nullptr ? containing->weak_from_this().lock() : nullptr; } +std::shared_ptr StorageTexture::FindPendingSlice(std::uint64_t address, std::uint64_t bytes) { + if (auto whole = FindPending(address, bytes)) return whole; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + StorageTexture* found = nullptr; + for (auto* texture : pending.textures) { + const auto base = texture->descriptor.baseAddress; + const auto layers = texture->geometry.imageLayers; + if (texture->geometry.thick || layers <= 1 || bytes == 0 || address <= base || texture->guestBytes % layers != 0 || texture->guestBytes / layers != bytes) continue; + if ((address - base) % bytes != 0 || (address - base) / bytes >= layers) continue; + if (found != nullptr) { + char message[256]; + std::snprintf(message, sizeof(message), "AGC graphics: 0x%llx+0x%llx is a slice of the pending array images 0x%llx and 0x%llx: which one holds its results is not modeled", static_cast(address), static_cast(bytes), static_cast(found->descriptor.baseAddress), static_cast(base)); + throw std::runtime_error(message); + } + found = texture; + } + return found != nullptr ? found->weak_from_this().lock() : nullptr; +} + bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except) { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -3523,7 +3829,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { return true; } -bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal) { +bool StorageTexture::FillClear(std::span pattern, std::uint32_t layer, const char*& refusal, bool allowRegisterClear) { // Debug aid (APS5_TRACE_FILL_COVER=1): the first layer covers refused, with the surface, the // pattern and the generation the refusal was memoized at (ClassifyFill's layerRefusedGeneration). static const bool trace = std::getenv("APS5_TRACE_FILL_COVER") != nullptr; @@ -3554,7 +3860,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u // Under a clear code reads see that value whatever the texels hold, and keys a recorded // kernel still writes are not in the bytes yet: only keys that read as uncompressed now let // the image stand for the texels. - if (Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { + if (!allowRegisterClear && Recorder::SnapshotWriteOverlaps(descriptor.dccAddress, static_cast(guestBytes / 256))) { refusal = "keys pending"; return false; } @@ -3604,7 +3910,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { if (texture == this || texture->released || !texture->overlaps(begin, bytes)) continue; - texture->layerOriginal.assign(texture->trackedLayers, false); + texture->forgetSavedOver(begin, bytes); if (texture->descriptor.dccAddress != 0) aliasedKeys = true; } } @@ -3738,7 +4044,7 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { auto& live = Live(); std::lock_guard lock(live.mutex); for (auto* texture : live.textures) { - if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false); + if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->forgetSavedOver(descriptor.baseAddress, static_cast(guestBytes)); } } GuestMemory::MarkWritten(descriptor.baseAddress, static_cast(guestBytes)); @@ -4101,7 +4407,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { const auto to = std::min(at + block, end); if (from >= to) continue; const bool edge = from != at || to != at + block; - if (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) { + if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) { skippedAny = true; skippedLayer[layer] = true; ++skipped; @@ -4111,6 +4417,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { else keep.emplace_back(from, to); } } + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer]) forgetKeptAtUnwatch(layer); + } // Debug aid: APS5_TRACE_FLUSH also names the blocks a write-back leaves to the CPU. static const bool traceKept = std::getenv("APS5_TRACE_FLUSH") != nullptr; if (traceKept && skippedAny) { @@ -4300,6 +4609,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } return; } + original.resize(static_cast(guestBytes)); // The store below compares against the guest bytes, read now when `original` does not hold // them (a GPU clear, a dropped or kept unit). Reading them says nothing about the image: a // stale unit (dropped, or kept for the CPU) still differs from them, so `original` vouches for diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index bed858e7a1..95ed372398 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -181,7 +181,10 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem createInfo.stage = stage; createInfo.layout = image ? imagePipelineLayout : pipelineLayout; VkPipeline result = VK_NULL_HANDLE; - Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &result), "vkCreateComputePipelines"); + } pipelines.emplace_back(key, result); return result; } @@ -299,7 +302,11 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineInfo.stage.module = cmaskModule; pipelineInfo.stage.pName = "main"; pipelineInfo.layout = cmaskPipelineLayout; - const auto created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline); + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &cmaskPipeline); + } context.Function("vkDestroyShaderModule")(context.device, cmaskModule, nullptr); Check(created, "vkCreateComputePipelines CMASK clear"); cmaskErrors = std::make_unique(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index fdee455cfb..ca7f4da8b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -42,7 +42,9 @@ constexpr FormatEntry kFormatLookup[] = { {29, VK_FORMAT_R16G16_SFLOAT, 4, false}, {36, VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4, false}, {50, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4, false}, + {51, VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4, false}, {54, VK_FORMAT_A2B10G10R10_UINT_PACK32, 4, false}, + {55, VK_FORMAT_A2B10G10R10_SINT_PACK32, 4, false}, {56, VK_FORMAT_R8G8B8A8_UNORM, 4, false}, {57, VK_FORMAT_R8G8B8A8_SNORM, 4, false}, {60, VK_FORMAT_R8G8B8A8_UINT, 4, false}, diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 4df2315361..736f3207fd 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -6,7 +6,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" COMMAND "$" -V --target-env vulkan1.1 -o "${agcColorTransferSpv}" "${agcColorTransferSource}" COMMAND ${CMAKE_COMMAND} -DINPUT=${agcColorTransferSpv} -DOUTPUT=${agcColorTransferHeader} -DSYMBOL=COLOR_TRANSFER_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" - DEPENDS "${agcColorTransferSource}" glslang-standalone + DEPENDS "${agcColorTransferSource}" "${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PqToSdr.glsl" glslang-standalone VERBATIM ) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 3af9d345d9..cf54e315dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -41,7 +41,7 @@ void RunBdaContractTests() { ValidateBdaTarget(program, target); AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); program.Resources().stage = IrShaderStage::TessellationControl; - reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + ValidateBdaTarget(program, target); program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 5a1f81054e..356fe0d4ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -243,6 +243,320 @@ void importWindowTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +void importJoinTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t large = 3 * piece; + void* block = ::operator new(3 * large + 5 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + std::memset(guest, 0x11, 3 * large); + const auto first = reinterpret_cast(block); + const auto second = first + large; + const auto third = second + large; + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, std::size_t bytes, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto bind = [&](std::uintptr_t address, std::size_t bytes = 32) { + GuestBufferMemory plain(importing); + plain.AddReadable(address, bytes); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(address, bytes, adjustment); + const auto copied = plain.CopiedBytes(); + plain.WriteBack(); + return std::pair{view.buffer, copied}; + }; + change(guest, large, true); + change(guest + large, large, true); + const auto* low = HostImportFor(importing, first, 16); + Require(low != nullptr && low->base == first && low->bytes == large, "a range larger than the import window was not imported alone"); + const auto* high = HostImportFor(importing, second, 16); + Require(high != nullptr && high->base == second && high->bytes == large, "the next range larger than the import window was not imported alone"); + Require(HostImportFor(importing, second - 16, 32) == nullptr, "a view across two imported ranges was imported before it was bound"); + { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 16, 32); + leased.Upload(true); + const auto* joined = HostImportFor(importing, second - 16, 32); + Require(joined != nullptr && joined->base == first && joined->bytes == 2 * large, "a buffer bound across two adjacent ranges larger than the import window was not imported as one"); + std::uint32_t adjustment = 0; + const auto crossing = leased.Descriptor(second - 16, 32, adjustment); + Require(crossing.buffer == joined->buffer && crossing.offset + adjustment == second - 16 - first, "a buffer bound across the joined ranges is not bound in place"); + leased.WriteBack(); + } + const auto joinedBuffer = HostImportFor(importing, first, 16)->buffer; + const auto [again, copied] = bind(second - 16); + Require(again == joinedBuffer && copied == 0, "a later bind across the joined ranges did not use the joined import"); + auto* heap = guest + 3 * large + 2 * piece; + change(heap, piece, true); + change(heap + piece, piece, true); + Require(bind(reinterpret_cast(heap + piece) - 16).second != 0 && HostImportFor(importing, reinterpret_cast(heap + piece) - 16, 32) == nullptr, "two pieces no larger than the import window were joined"); + const auto heapBase = reinterpret_cast(heap); + const auto [piecesBuffer, piecesCopied] = bind(heapBase, 2 * piece); + const auto* pieces = HostImportFor(importing, heapBase, 2 * piece); + Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one"); + change(heap + 2 * piece, piece, true); + static_cast(bind(heapBase + 2 * piece - 16)); + const auto* kept = HostImportFor(importing, first, 16); + Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges"); + change(guest + 2 * large, large, true); + Require(HostImportFor(importing, first, 16)->buffer == joinedBuffer, "a range registered next to the joined ranges re-imported them"); + const auto [grownBuffer, grownCopied] = bind(third - 16); + const auto* grown = HostImportFor(importing, third - 16, 32); + Require(grown != nullptr && grown->base == first && grown->bytes == 3 * large && grownBuffer == grown->buffer && grownCopied == 0, "a bind across the next range did not grow the joined import"); + Require(bind(second - 16).first == grownBuffer && HostImportFor(importing, first, 16)->buffer == grownBuffer, "two joins sharing a range retired each other"); + change(heap, piece, false); + change(heap + piece, piece, false); + change(heap + 2 * piece, piece, false); + change(guest + large, large, false); + const auto* alone = HostImportFor(importing, first, 16); + Require(alone != nullptr && alone->base == first && alone->bytes == large, "the joined import outlived one of its ranges"); + change(guest, large, false); + change(guest + 2 * large, large, false); + Require(HostImportFor(importing, first, 16) == nullptr && HostImportFor(importing, third, 16) == nullptr, "the host imports outlived their ranges"); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the +// test and reset after it. +class WindowedRange { +public: + WindowedRange(const Context& context, std::size_t window, std::size_t cells, std::uint64_t budget = 0) : window(window), bytes(window * cells), importing(context) { + block = ::operator new(bytes, std::align_val_t{piece}); + std::memset(block, 0x33, bytes); + importing.hostImportAlignment = piece; + SetHostImportRangeWindow(window); + SetHostImportBudget(budget); + change(true); + } + ~WindowedRange() { + change(false); + SetHostImportBudget(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ::operator delete(block, std::align_val_t{piece}); + } + WindowedRange(const WindowedRange&) = delete; + WindowedRange& operator=(const WindowedRange&) = delete; + + std::uintptr_t First() const { return reinterpret_cast(block); } + + static constexpr std::size_t piece = 65536; + const std::size_t window; + const std::size_t bytes; + Context importing; + +private: + void change(bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + + void* block = nullptr; +}; + +// A registered range larger than the import window, its first window imported by an earlier access: an address-based +// build (AcquireRegistered) must import the whole range, so a view in its last window binds (Demon's Souls threw +// "guest buffer view ... exceeds its GPU owner" with the 64 MiB windows). +void importWholeRangeOverWindowTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 2); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->base == first && windowed->bytes == range.window, "an access to the first window of a large range was not imported as that window"); + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(first + range.bytes - WindowedRange::piece + 16, 32, adjustment); + Require(view.range == 32 + adjustment, "a view in the last window of a large registered range has no descriptor"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && view.buffer == whole->buffer, "an address-based build was served by the range's first window, not an import of the whole range"); + leased.WriteBack(); +} + +// An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before, +// every such build retired the window and the next access imported it again. +void importRefusedWholeRangeTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 4, 3 * 2 * WindowedRange::piece); + const auto first = range.First(); + const auto* windowed = HostImportFor(range.importing, first + 16, 32); + Require(windowed != nullptr && windowed->bytes == range.window, "the first window of a range over the budget was not imported"); + const auto buffer = windowed->buffer; + for (int build = 0; build < 3; ++build) { + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + Require(HostImportCovers(range.importing, first + 16, 32), "an address-based build refused its whole-range import and retired the range's window (build " + std::to_string(build) + ")"); + } + const auto* kept = HostImportFor(range.importing, first + 16, 32); + Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import"); +} + +// Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows +// the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at +// every use. +void importNeighbouringEdgesTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 3); + const auto first = range.First(); + const auto use = [&](std::uintptr_t address) { + GuestBufferMemory leased(range.importing); + leased.AddReadable(address, 32); + leased.Upload(false); + leased.WriteBack(); + }; + const auto acrossFirst = first + range.window - 16; + const auto acrossSecond = first + 2 * range.window - 16; + use(acrossFirst); + Require(HostImportCovers(range.importing, acrossFirst, 32), "a use across the first cell edge was not imported"); + use(acrossSecond); + Require(HostImportCovers(range.importing, first, range.bytes), "a use across the second cell edge did not grow the first use's window to their union"); + const auto serial = HostImportSerial(range.importing, first, range.bytes, false); + for (int round = 0; round < 10; ++round) { + use(acrossFirst); + use(acrossSecond); + Require(HostImportSerial(range.importing, first, range.bytes, false) == serial, "a use across a cell edge replaced the shared window (round " + std::to_string(round) + ")"); + } +} + +// A range mapped while the imports are at the budget is refused at map time without retiring an import: the map +// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit). +void importAtMapOverBudgetTests(const Context& context) { + constexpr std::size_t bytes = 65536; + auto importing = context; + importing.hostImportAlignment = bytes; + void* kept = ::operator new(bytes, std::align_val_t{bytes}); + void* used = ::operator new(bytes, std::align_val_t{bytes}); + void* mapped = ::operator new(bytes, std::align_val_t{bytes}); + const auto change = [&](void* block, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + change(kept, true); + change(used, true); + const auto keptAddress = reinterpret_cast(kept); + const auto usedAddress = reinterpret_cast(used); + Require(HostImportFor(importing, keptAddress, 16) != nullptr, "the import to keep was not made"); + for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, usedAddress, 16) != nullptr, "the import in use was not made"); + { + GuestBufferMemory leased(importing); + leased.AddReadable(usedAddress, 16); + leased.Upload(false); + leased.WriteBack(); + } + const auto live = HostImportLiveBytes(); + SetHostImportBudget(live + bytes - 1); + change(mapped, true); + GuestAllocations::Mapped ranges; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& range : lease) { + if (range->address == reinterpret_cast(mapped)) ranges.push_back(range); + } + } + Require(ranges.size() == 1, "the mapped range is not registered"); + static_cast(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true)); + SetHostImportBudget(0); + Require(HostImportLiveBytes() == live, "a range mapped at the budget retired an import at map time (an eviction without the GPU lock)"); + Require(HostImportCovers(importing, keptAddress, 16) && !HostImportCovers(importing, reinterpret_cast(mapped), 16), "a range mapped at the budget was imported, or the import to keep was retired"); + for (auto* block : {kept, used, mapped}) change(block, false); + for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes}); +} + +void spaceReadAcrossImportsTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto boundary = first + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(boundary - 256, 512); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(boundary - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a read-only V# across two imports has no descriptor"); + const auto bytes = access.bytes(view.buffer); + Require(view.offset + adjustment + 512 <= bytes.size() && bytes[view.offset + adjustment] == std::byte{0x5a} && bytes[view.offset + adjustment + 256] == std::byte{0x6b}, "a read-only V# across two imports misses the bytes of either range"); + const auto ranges = leased.AddressRanges(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && boundary <= range.end; }) && std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= boundary && boundary + piece <= range.end; }), "an import is missing from the BDA table of a build with a read-only V# across it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a read-only V# across two imports dissolved the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + +void spaceReadAcrossImportsWrittenTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto boundary = reinterpret_cast(block) + window; + std::memset(guest, 0x5a, window); + std::memset(guest + window, 0x6b, piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (const bool writtenFirst : {false, true}) { + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + if (writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.AddReadable(boundary - 256, 512); + if (!writtenFirst) leased.AddWritable(boundary - 384, 256); + leased.Upload(true); + std::uint32_t readAdjustment = 0; + std::uint32_t writeAdjustment = 0; + const auto read = leased.Descriptor(boundary - 256, 512, readAdjustment); + const auto written = leased.Descriptor(boundary - 384, 256, writeAdjustment); + Require(read.buffer != VK_NULL_HANDLE && read.buffer == written.buffer && written.offset + writeAdjustment + 128 == read.offset + readAdjustment, "a written V# inside a read-only V# across two imports maps other bytes than it"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 0) Require(after.dissolvedOverlap == before.dissolvedOverlap + 1, "a written V# inside a read-only V# across two imports kept the cached address space"); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + } + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1240,6 +1554,13 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { failedImportRemapTests(context, access); spaceImportRunTests(context); importWindowTests(context); + spaceReadAcrossImportsTests(context, access); + spaceReadAcrossImportsWrittenTests(context); + importJoinTests(context); + importWholeRangeOverWindowTests(context); + importRefusedWholeRangeTests(context); + importNeighbouringEdgesTests(context); + importAtMapOverBudgetTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 40786cc1e2..c77bdb157f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -210,6 +210,10 @@ void RunColorTargetLayoutTests() { } Require(DecodeColorTileMode(0x0dc68000u) == ColorTileMode::D64KBX, "the observed SW_64KB_D_X color descriptor was rejected"); + Require(DecodeColorTileMode(0x4dc60000u) == ColorTileMode::Z64KBX, "the observed SW_64KB_Z_X color descriptor was rejected"); + Require(ColorTextureTileMode(ColorTileMode::Z64KBX) == TextureTileMode::kZ64KBX, "a Z-order XOR color target did not map to its texture mode"); + Require(ColorTileModeIsXor(ColorTileMode::Z64KBX) && ColorTileModeBlockBytes(ColorTileMode::Z64KBX) == 65536u, "a Z-order XOR color target lost its block alignment or XOR flag"); + reject([] { DecodeColorTileMode((0x4dc60000u & ~(0x1fu << 19u)) | (1u << 19u)); }); Require(ColorTextureTileMode(ColorTileMode::D64KBX) == TextureTileMode::kD64KBX, "a display XOR color target did not map to its texture mode"); Require(ColorTileModeIsXor(ColorTileMode::D64KBX) && ColorTileModeBlockBytes(ColorTileMode::D64KBX) == 65536u, "a display XOR color target lost its block alignment or XOR flag"); reject([] { DecodeColorTileMode(0x0cc68000u); }); @@ -230,7 +234,7 @@ void RunColorTargetLayoutTests() { const auto block = ThinBlockLayout(TextureTileMode::kD64KBX, bpe); const auto width = 2u * block[1] + 1u; const auto height = 2u * block[2] + 1u; - for (const auto mode : {ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { + for (const auto mode : {ColorTileMode::Z64KBX, ColorTileMode::D64KBX, ColorTileMode::RenderTarget}) { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bpe); Require(equation != nullptr, "a 64 KiB color swizzle equation is missing"); const ColorTargetLayout layout(width, height, mode, bpe, 0x4600); diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index a78c75f9bc..ef18897afc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -342,16 +343,10 @@ void verifyLargeLoopExitFallback() { static_cast(recompile(name, code, 64u)); } -void verifyNestedExitForwarders() { +std::vector branchProgram(std::span> targets, std::span order) { constexpr auto none = InvalidControlFlowId; - constexpr std::array, 21> targets{{ - {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, - {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, - {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, - }}; - constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; std::vector code; - std::array starts{}; + std::vector starts(targets.size()); std::vector> patches; for (std::size_t at = 0; at < order.size(); ++at) { const auto block = order[at]; @@ -377,19 +372,45 @@ void verifyNestedExitForwarders() { } for (const auto& [instruction, target] : patches) { const auto displacement = static_cast(starts[target]) - static_cast(instruction) - 1; - require(displacement >= -32768 && displacement <= 32767, "nested exit forwarder test branch is out of range"); + require(displacement >= -32768 && displacement <= 32767, "a generated test branch is out of range"); code[instruction] |= static_cast(displacement) & 0xffffu; } + return code; +} + +void verifyNestedExitForwarders() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 21> targets{{ + {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, + {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, + {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, + }}; + constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; + const auto code = branchProgram(targets, order); const std::string name("nested exit forwarders keep conditional selections structured"); static_cast(verifyGraph(name, code)); static_cast(recompile(name, code, 32u)); static_cast(recompile(name, code, 64u)); } +void verifyIrreducibleLoopEntry() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 4> targets{{{1, 2}, {2, 3}, {1, 3}, {none, none}}}; + constexpr std::array order{0, 1, 2, 3}; + const auto code = branchProgram(targets, order); + const std::string name("a loop entered at either of its two blocks runs behind reach flags"); + const auto result = verifyGraph(name, code); + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + void verifyProgram(const Program& program) { const std::string name(program.name); const auto result = verifyGraph(name, program.code); - if (program.split == Split::None) { + if (std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr) { + require(result.clonedInstructions == 0 && result.routeVariables != 0, name + ": expected the reach flag fallback, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); + } else if (program.split == Split::None) { require(result.clonedInstructions == 0 && result.routeVariables == 0, name + ": expected no clone or routing, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); } else if (program.split == Split::Clone) { require(result.clonedInstructions != 0 && result.routeVariables == 0, name + ": expected a clone, got " + std::to_string(result.clonedInstructions) + " cloned instructions and " + std::to_string(result.routeVariables) + " route variables"); @@ -480,6 +501,38 @@ void verifyNullSwappc() { } +void VerifyUserDataTail() { + const RdnaInstructionDecoder decoder; + const std::vector code{0x7e000280u, 0xbe802008u, 0xbf9f0000u}; + const auto decoded = decoder.Decode(code); + const auto tail = FindUserDataTail(decoded, 8u, 2u); + require(tail && tail->programCounter == 4u && tail->userDataRegister == 8u, "unchanged user-data tail was not recognized"); + require(!FindUserDataTail(decoded, 9u, 2u) && !FindUserDataTail(decoded, 8u, 1u), "tail outside user data was accepted"); + const std::vector> refused{ + {0xbe880380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe890380u, 0xbe802008u, 0xbf9f0000u}, + {0xbe880480u, 0xbe802008u, 0xbf9f0000u}, + {0xf4080200u, 0xfa000000u, 0xbe802008u, 0xbf9f0000u}, + {0xbf820000u, 0xbe802008u, 0xbf9f0000u}, + {0xbe802009u, 0xbf9f0000u}, + {0xbe802008u, 0xbf810000u}, + {0xbe802008u, 0xbf800000u, 0xbf9f0000u}, + }; + for (const auto& rejected : refused) require(!FindUserDataTail(decoder.Decode(rejected), 8u, 4u), "a modified, branching or malformed tail was accepted"); + const std::vector continuation{0xbf820001u, 0x7e000281u, 0xbf810000u}; + const auto linked = LinkUserDataTail(decoded, *tail, decoder.Decode(continuation)); + require(linked == std::vector({0x7e000280u, 0xbf820000u, 0xbf820001u, 0x7e000281u, 0xbf810000u}), "linking did not preserve relative branches and register operations"); + static_cast(GraphBuilder{}.Build(decoder.Decode(linked))); + const std::vector readsPc{0xbe8c1f00u, 0xbf810000u}; + bool refusedPc = false; + try { + static_cast(LinkUserDataTail(decoded, *tail, decoder.Decode(readsPc))); + } catch (const std::invalid_argument& error) { + refusedPc = std::string_view(error.what()).find("reads its program counter") != std::string_view::npos; + } + require(refusedPc, "a continuation reading its relocated PC was accepted"); +} + int main(int argc, char** argv) { try { verifyLoopMergeDoesNotEnterNestedSelection(); @@ -1279,8 +1332,10 @@ done: } try { verifyNullSwappc(); + VerifyUserDataTail(); verifyLargeLoopExitFallback(); verifyNestedExitForwarders(); + verifyIrreducibleLoopEntry(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index 47020acb0a..3db6a8ce08 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -150,6 +150,66 @@ int main() { return b.eliminate() == 1u && !clamped.HasUses() && product.HasUses(); }); + passed &= run("a min, max, min3, max3 or med3 of flushed values needs no flush, one with an unknown operand does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto flushedResult = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + return b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(opcode, IrType::F32, arguments)})); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& z = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = { + &flushedResult(IrOpcode::FPMin32, {&x, &y}), + &flushedResult(IrOpcode::FPMax32, {&y, &x}), + &flushedResult(IrOpcode::FPMinTri32, {&x, &y, &z}), + &flushedResult(IrOpcode::FPMaxTri32, {&z, &y, &x}), + &flushedResult(IrOpcode::FPMedTri32, {&x, &z, &y}), + }; + auto& mixedPair = flushedResult(IrOpcode::FPMax32, {&x, &raw}); + auto& mixedMedian = flushedResult(IrOpcode::FPMedTri32, {&x, &y, &raw}); + for (auto* flushed : removed) b.keep(*flushed); + b.keep(mixedPair); + b.keep(mixedMedian); + const bool counted = b.eliminate() == 5u; + bool kept = false; + for (auto* flushed : removed) kept |= flushed->HasUses(); + return counted && !kept && mixedPair.HasUses() && mixedMedian.HasUses(); + }); + + passed &= run("a floor or ceil needs no flush, even of an unknown value", [] { + Builder b; + auto& raw = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& floored = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPFloor32, IrType::F32, {&raw})})); + auto& ceiled = b.flush(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPCeil32, IrType::F32, {&raw})})); + b.keep(floored); + b.keep(ceiled); + return b.eliminate() == 2u && !floored.HasUses() && !ceiled.HasUses(); + }); + + passed &= run("a max carried around a loop without a flush keeps its flush and the search ends", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + head.AddBranch(&body); + body.AddBranch(&body); + body.AddBranch(&exit); + auto& initial = b.flush(b.unknown()); + b.block = &body; + auto& carried = b.program.CreateValue(IrOpcode::Phi, IrType::U32); + body.AppendInstruction(&carried); + auto& step = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.flush(b.unknown())}); + auto& maximum = b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&b.emit(IrOpcode::FPMax32, IrType::F32, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&carried}), &step})}); + carried.AddPhiOperand(&head, &initial); + carried.AddPhiOperand(&body, &maximum); + auto& flushed = b.flush(maximum); + b.block = &exit; + b.keep(flushed); + return b.eliminate() == 0u && flushed.HasUses(); + }); + passed &= run("a phi of flushed values needs no flush", [] { Builder b; IrBlock& head = *b.block; diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index ff2d18e119..deec634f74 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include namespace { @@ -35,10 +37,13 @@ THandle newHandle() { } VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; +std::vector createdImages; +std::vector> imageCopies; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; *image = newHandle(); + createdImages.push_back(*image); return VK_SUCCESS; } @@ -97,6 +102,9 @@ VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCal VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); +} VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { *properties = {}; @@ -133,7 +141,8 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkDestroyImage", reinterpret_cast(destroyImage)}, {"vkFreeMemory", reinterpret_cast(freeMemory)}, {"vkCmdPipelineBarrier", reinterpret_cast(cmdPipelineBarrier)}, - {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)} + {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)}, + {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -276,5 +285,28 @@ void RunDepthSurfaceReuseTests() { Require(storageAliasesDepth(context, {stencil16, FormatR8Uint, 128, 128}), "storage access to the actual stencil plane must remain rejected"); Require(!storageAliasesDepth(context, {stencil16, FormatR11G11B10Float, 128, 128}), "a stencil address reused for color is not a stencil storage alias"); + constexpr std::uint64_t shared = 0x70000000; + constexpr std::uint64_t stencilA = 0x71000000; + constexpr std::uint64_t stencilB = 0x72000000; + constexpr std::uint64_t otherDepth = 0x73000000; + const auto pairing = [&](std::uint64_t depth, std::uint64_t stencil) { + const auto before = createdImages.size(); + DepthSurfaceView(context, {depth, stencil, {256, 256}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + return createdImages.size() != before ? createdImages[before] : VK_NULL_HANDLE; + }; + const auto withA = pairing(shared, stencilA); + imageCopies.clear(); + const auto withB = pairing(shared, stencilB); + Require(withA != VK_NULL_HANDLE && withB != VK_NULL_HANDLE && withA != withB, "each depth/stencil pairing must have its own image"); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, withB, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "a depth plane paired with another stencil plane must carry its depth over"); + imageCopies.clear(); + pairing(shared, stencilB); + Require(imageCopies.empty(), "drawing again with the same pairing must not copy"); + pairing(shared, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withB, withA, static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)), "returning to the first pairing must carry the depth written through the second"); + imageCopies.clear(); + const auto otherWithA = pairing(otherDepth, stencilA); + Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + ClearDepthSurfaces(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp index c139312bf4..2ed8ec1ba9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DeviceMemoryFallback.cpp @@ -22,11 +22,15 @@ constexpr std::uint64_t MiB = 1ull << 20u; std::uint64_t reportedBudget = 0; std::uint64_t reportedUsage = 0; +std::uint64_t reportedHostBudget = 0; +std::uint64_t reportedHostUsage = 0; VKAPI_ATTR void VKAPI_CALL mockMemoryProperties2(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { auto* reported = static_cast(properties->pNext); reported->heapBudget[0] = reportedBudget; reported->heapUsage[0] = reportedUsage; + reported->heapBudget[1] = reportedHostBudget; + reported->heapUsage[1] = reportedHostUsage; } Context mockContext(bool budgetExtension) { @@ -49,6 +53,46 @@ void Choose(const char* what, bool budgetExtension, std::uint64_t budget, std::u Expect(type == expected, std::string(what) + ": chose memory type " + std::to_string(type) + ", expected " + std::to_string(expected)); } +void BufferPlacementTests() { + constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + Context context = mockContext(true); + context.memory.memoryTypeCount = 4; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.memory.memoryTypes[2] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[3] = {VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1}; + reportedBudget = 3000 * MiB; + reportedUsage = 2936 * MiB; + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + const auto choose = [&](const char* what, std::uint32_t mask, VkMemoryPropertyFlags flags, std::uint32_t expected) { + const auto type = context.BufferMemoryType(mask, flags, 64 * MiB); + Expect(type == expected, std::string(what) + ": chose " + std::to_string(type) + ", expected " + std::to_string(expected)); + }; + choose("coherent buffer fits exactly in preferred cached heap", 15, coherent, 0); + ++reportedUsage; + choose("coherent buffer moves before allocating from a full heap", 15, coherent, 1); + choose("buffer respects its allowed type mask", 5, coherent, 0); + choose("another type on the same full heap is not an alternative", 7, coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0); + choose("noncoherent alternative is not eligible", 9, coherent, 0); + choose("explicit device-local requirement is preserved", 15, coherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0); + choose("uncached-only coherent buffer preserves its type", 2, coherent, 1); + reportedHostUsage = reportedHostBudget - 64 * MiB + 1; + choose("alternative must have enough reported room", 15, coherent, 0); + reportedHostBudget = 0; + choose("unreported alternative budget is not assumed free", 15, coherent, 0); + reportedHostBudget = 8000 * MiB; + reportedHostUsage = 2000 * MiB; + reportedBudget = 0; + choose("unreported preferred budget retains cached preference", 15, coherent, 0); + reportedBudget = 3000 * MiB; + context.memoryProperties2 = nullptr; + choose("no budget query retains cached preference", 15, coherent, 0); + context.memoryProperties2 = mockMemoryProperties2; + context.physical = VK_NULL_HANDLE; + choose("no physical device retains cached preference", 15, coherent, 0); +} + } int main() { @@ -61,6 +105,7 @@ int main() { Choose("allocation exceeds the budget", true, 3000 * MiB, 2937 * MiB, both, HostType); Choose("usage already over the budget", true, 2952 * MiB, 3633 * MiB, both, HostType); Choose("no host type allowed", true, 3000 * MiB, 3000 * MiB, 1u << DeviceType, DeviceType); + BufferPlacementTests(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp index f5f1f159ff..a257da198b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -264,6 +264,14 @@ int main() { ExpectPixels(buffer, DccKeys::ClearRegister, {0, 0, 255, 255}, "A2B10G10R10 register clear"); buffer.pixelFormat = bgra | tenBit; ExpectPixels(buffer, DccKeys::ClearRegister, {255, 0, 0, 255}, "A2R10G10B10 register clear"); + buffer.pixelFormat = 0x8100070422000000ull; + ExpectPixels(buffer, DccKeys::Clear0000, {0, 0, 0, 255}, "R10G10B10A2 PQ 0000"); + ExpectPixels(buffer, DccKeys::Clear1111, {255, 255, 255, 255}, "R10G10B10A2 PQ 1111"); + buffer.dccClearColor = 600u | (500u << 10u) | (300u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "R10G10B10A2 PQ register clear"); + buffer.pixelFormat = 0x8100070400000000ull; + buffer.dccClearColor = 300u | (500u << 10u) | (600u << 20u) | (3u << 30u); + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 213, 255, 255}, "B10G10R10A2 PQ register clear"); for (const auto keys : {DccKeys::Uncompressed, DccKeys::Mixed, DccKeys::Unreadable}) { const auto message = Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, keys); }); Expect(message.find(AgcDriver::Graphics::DccKeysName(keys)) != std::string::npos && message.find("0x7f0000") != std::string::npos, std::string("keys without a clear value were presented as one: ") + AgcDriver::Graphics::DccKeysName(keys)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp index 4035a58490..baf3155a63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Float16Conversion.cpp @@ -42,7 +42,7 @@ std::vector recompile(std::span capabilities request.useCache = false; auto result = Recompile(request); require(!result.spirv.empty(), "v_cvt_f16_f32 did not recompile"); - return std::move(result.spirv); + return result.spirv.Copy(); } struct Usage { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 29a7dc8b2a..89a5761f97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" @@ -22,10 +23,12 @@ #include "BdaAbi.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include #include #include #include +#include #include #include #include @@ -99,6 +102,90 @@ void expectFailure(TAction action, std::string_view reason) { throw std::runtime_error("expected graphics rejection: " + std::string(reason)); } +void inactiveVertexPointSizeTests() { + const auto pointSizeState = [] { + auto queue = makeState(); + queue.context[0x1b3] = queue.context[0x1b4] = 2; + queue.context[0x207] = 0x01210000; + return queue; + }; + for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + for (const auto raster : {0u, 0x240u, 0x248u}) { + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive vertex point size failed the draw precheck: " + rejection); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } + } + for (const auto raster : {0u, 0x240u, 0x248u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = 3; + queue.context[0x205] = raster; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "inactive line-strip point size failed the draw precheck: " + rejection); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.primitiveRestart, "inactive point size changed line-strip decoding"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "inactive point size changed the line primitive class"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + const auto indexedRejection = AgcDriver::Graphics::DrawRejection(queue, true); + Require(indexedRejection.empty(), "inactive line-strip point size with primitive restart failed the draw precheck: " + indexedRejection); + const auto indexedState = AgcDriver::Graphics::DecodeState(queue); + Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && indexedState.primitiveRestart, "inactive point size changed indexed line-strip decoding"); + } + const auto rejected = [](AgcDriver::QueueState queue) { + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("auxiliary vertex exports") != std::string::npos, "unsupported vertex exports passed the draw precheck"); + try { + static_cast(AgcDriver::Graphics::DecodeState(queue)); + } catch (const std::runtime_error&) { + return; + } + throw std::runtime_error("potentially active point size passed the state decoder"); + }; + for (const auto primitive : {1u, 9u}) { + auto queue = pointSizeState(); + queue.userConfig[0x242] = primitive; + rejected(queue); + } + for (const auto stages : {0x2004u, 0x2020u}) { + auto queue = pointSizeState(); + queue.context[0x2d5] = stages; + rejected(queue); + } + for (const auto raster : {8u, 0x208u, 0x48u}) { + auto queue = pointSizeState(); + queue.context[0x205] = raster; + rejected(queue); + } + for (const auto flag : {1u << 17u, 1u << 20u}) { + auto queue = pointSizeState(); + queue.context[0x207] = 0x01210000 | flag; + rejected(queue); + } + for (const auto distances : {1u, 0x100u, 0x00c000ffu, 0x00c0ff00u}) { + auto queue = pointSizeState(); + queue.context[0x207] |= distances; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "inactive point size rejected supported clip/cull distances"); + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == queue.context[0x207], "inactive point size discarded clip/cull distance state"); + } + { + auto queue = pointSizeState(); + queue.context[0x207] |= 0x004001ffu; + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "inactive point size bypassed the clip/cull count limit"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + } + for (const auto offset : {0x2d5u, 0x205u}) { + auto queue = pointSizeState(); + queue.context.erase(offset); + rejected(queue); + } + auto queue = pointSizeState(); + queue.userConfig.erase(0x242); + rejected(queue); +} + void hdrComponentSwapTests() { using namespace AgcDriver::Graphics; for (const auto number : {0u, 1u, 4u, 7u}) { @@ -452,6 +539,58 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnwrittenSampleLocationTests() { + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + using AgcDriver::Graphics::SampleLocations; + const auto fresh = [](std::uint32_t config, std::uint32_t attributes, std::uint32_t scanMode = 3) { + auto written = makeState(); + written.context[0x2f8] = config; + written.context[0x292] = scanMode; + written.context[0x3b8] = 0x0dc00000u; + written.context[0x31d] = attributes; + written.context[0x1b3] = 2; + written.context[0x1b4] = 2; + AgcDriver::QueueState queue; + queue.shader = written.shader; + queue.userConfig = written.userConfig; + written.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { queue.context[offset] = value; }); + return queue; + }; + AgcDriver::QueueState initial; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(initial.context.contains(offset) && initial.context.at(offset) == 0, "an unwritten PA_SC_AA_SAMPLE_LOCS register did not hold its clear-state value 0"); + for (const auto samples : {2u, 4u, 8u}) { + const auto log2 = static_cast(std::countr_zero(samples)); + auto queue = fresh((log2 << 20u) | log2, (log2 << 12u) | (log2 << 15u)); + const auto state = DecodeState(queue); + Require(state.samples == samples && state.colors.size() == 1 && state.colors[0].samples == samples, "a multisampled draw with unwritten sample locations did not decode its sample count"); + Require(state.customSampleLocations, "unwritten sample locations did not decode as the clear-state pattern"); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(state.sampleLocations[sample] == 0, "an unwritten sample location was not at the pixel center"); + const auto centered = SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) Require(centered[sample].x == 0.5f && centered[sample].y == 0.5f, "unwritten sample locations did not place every sample at the pixel center"); + const auto rejection = DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused a multisampled draw with unwritten sample locations: " + rejection); + } + auto queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + auto state = DecodeState(queue); + Require(state.samples == 4 && !state.customSampleLocations, "explicit standard 4x sample locations changed"); + queue = fresh(0x00200002, 0x12000); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "explicit custom 4x sample locations changed"); + const auto custom = SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(custom[sample].x == expected[sample].x && custom[sample].y == expected[sample].y, "explicit custom 4x sample coordinates changed"); + queue.ClearContext(); + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) Require(queue.context.at(offset) == 0, "CLEAR_STATE did not reset PA_SC_AA_SAMPLE_LOCS"); + expectFailure([&] { DecodeState(fresh(0x00400004, 0x12000)); }, "more than eight samples"); + expectFailure([&] { DecodeState(fresh(0x00100002, 0x12000)); }, "exposing fewer samples"); + queue = fresh(0, 0, 3); + state = DecodeState(queue); + Require(state.samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target with unwritten sample locations was refused"); +} + void MsaaEnableSingleSampleTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -493,9 +632,15 @@ void MsaaEnableSingleSampleTests() { queue.context.erase(0x306); expectFailure([&] { DecodeState(queue); }, "missing register"); queue = enabled(); + queue.context[0x203] &= ~0x800u; queue.context[0x2dc] = 0xaa01; refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); queue = enabled(); + queue.context[0x2dc] = 0xaa01; + refused(queue, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); + queue.context[0x2dc] = 0x18701; + Require(!DecodeState(queue).alphaToCoverage && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target ignored the dithered alpha disable override"); + queue = enabled(); queue.context[0x313] = 0x6001; refused(queue, "MSAA_ENABLE on a single-sample target with conservative rasterization"); for (const auto primitive : {2u, 7u, 17u}) { @@ -515,6 +660,7 @@ void MsaaEnableSingleSampleTests() { queue.context[0x292] = 7; refused(queue, "scan conversion mode"); clear.context[0x2fe] = 0x44; + clear.context[0x203] &= ~0x800u; clear.context[0x2dc] = 0xaa01; Require(DecodeState(clear).alphaToCoverage && DrawRejection(clear, false).empty(), "a single-sample target without MSAA_ENABLE was refused over its sample locations or alpha-to-coverage"); } @@ -573,6 +719,34 @@ void ShaderStageTests() { fan.userConfig[0x242] = 3; expectFailure([&] { AgcDriver::Graphics::DecodeState(fan); }, "unsupported geometry input or output assembly"); } + { + auto fast = makeState(); + fast.userConfig[0x242] = 1; + fast.context[0x2d5] = 0x482030; + fast.userConfig[0x25b] = 0x201; + fast.context[0x1ff] = 32; + fast.context[0x2ce] = 32; + fast.context[0x2d3] = 64; + fast.context[0x29b] = 2; + fast.context[0x2ab] = 4; + fast.shader[0x8a] = 0x022c0245; + fast.shader[0x8b] = 0x9002a; + const auto state = AgcDriver::Graphics::DecodeState(fast); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry"); + const auto& mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR"); + fast.context[0x2d5] = 0x502030; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state"); + fast.context[0x2d5] = 0x482030; + fast.userConfig[0x25b] = 0x402; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x25b] = 0x201; + fast.userConfig[0x242] = 4; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + fast.userConfig[0x242] = 1; + fast.context[0x2d3] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; @@ -582,6 +756,7 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x200d; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "Patch topology and HS_EN disagree"); queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; queue.context[0x2d6] = 0xc355; queue.context[0x2db] = 0x40049; queue.context[0x286] = 0x42800000; @@ -599,17 +774,26 @@ void ShaderStageTests() { } Require(stages.path == AgcDriver::Graphics::ShaderPath::Tessellation && stages.tessellation, "tessellation routing changed"); const auto& tessellation = *stages.tessellation; - Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85, "the passthrough triangle-patch configuration decoded differently"); - for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { + Require(tessellation.inputControlPoints == 3 && tessellation.outputControlPoints == 3 && tessellation.domain == 1 && tessellation.partitioning == 2 && tessellation.outputTopology == 2 && stages.minTessellationLevel == 1.0f && stages.maxTessellationLevel == 64.0f && tessellation.patchesPerGroup == 85 && tessellation.primitiveGroupSize == 85 && !tessellation.breakAtInstance, "the passthrough triangle-patch configuration decoded differently"); + for (const auto [parameters, reason] : {std::pair{0x44049u, "VGT_TF_PARAM"}, std::pair{0x140049u, "VGT_TF_PARAM"}, std::pair{0x2040049u, "VGT_TF_PARAM"}, std::pair{0x40059u, "fractional-odd partitioning"}, std::pair{0x4004du, "fractional-odd partitioning"}, std::pair{0x40045u, "fractional-odd partitioning"}, std::pair{0x40048u, "fractional-odd partitioning"}, std::pair{0x40029u, "fractional-odd partitioning"}, std::pair{0x4004bu, "fractional-odd partitioning"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } + queue.context[0x2db] = 0x40041; + Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->partitioning == 0, "integer-partitioned triangle patches decoded differently"); queue.context[0x2d6] = 0x1043f; queue.context[0x2db] = 0x4006a; queue.context[0x286] = 0x41100000; + queue.userConfig[0x25b] = 0x5fe3f; const auto quadStages = AgcDriver::Graphics::DecodeState(queue).stages; const auto& quads = quadStages.tessellation; - Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63, "counter-clockwise quad patches with HOS levels [1, 9] and 63 patches per group decoded differently"); + Require(quads && quads->inputControlPoints == 4 && quads->outputControlPoints == 4 && quads->domain == 2 && quads->partitioning == 2 && quads->outputTopology == 3 && quadStages.minTessellationLevel == 1.0f && quadStages.maxTessellationLevel == 9.0f && quads->patchesPerGroup == 63 && quads->primitiveGroupSize == 63 && quads->breakAtInstance, "counter-clockwise quad patches with HOS levels [1, 9], 63 patches per group and BREAK_WAVE_AT_EOI decoded differently"); + queue.userConfig[0x25b] = 0x8040; + const auto shared = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(shared && shared->primitiveGroupSize == 64 && !shared->breakAtInstance, "GE_CNTL 0x8040 did not decode as 64 primitives per group with BREAK_WAVE_AT_EOI clear"); + queue.userConfig.erase(0x25b); + const auto missing = AgcDriver::Graphics::DecodeState(queue).stages.tessellation; + Require(missing->primitiveGroupSize == 0 && !missing->breakAtInstance, "a draw without GE_CNTL decoded a primitive group size or BREAK_WAVE_AT_EOI"); queue.context[0x2db] = 0x40069; Require(AgcDriver::Graphics::DecodeState(queue).stages.tessellation->outputTopology == 3, "counter-clockwise triangle patches decoded differently"); for (const auto [maximum, minimum, reason] : {std::tuple{0x3f800000u, 0x40000000u, "tessellation level clamps"}, std::tuple{0x41100000u, 0xbf800000u, "tessellation level clamps"}, std::tuple{0x3f000000u, 0x00000000u, "tessellation level clamps"}, std::tuple{0x42800000u, 0x42800000u, "tessellation level clamps"}, std::tuple{0x7fc00000u, 0x3f800000u, "non-finite register"}, std::tuple{0x7f800000u, 0x3f800000u, "non-finite register"}}) { @@ -717,10 +901,10 @@ bool readsBuiltin(const std::vector& read, spv::BuiltIn builtin) { return std::find(read.begin(), read.end(), builtin) != read.end(); } -std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) { +std::vector pixelNoPerspectiveLocations(bool barycentricEnabled, std::uint32_t inputs = 0x22u) { auto queue = makeState(); - queue.context[0x1b3] = 0x22u; - queue.context[0x1b4] = 0x22u; + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; queue.context[0x1b6] = 2u; queue.context[0x191] = 0u; queue.context[0x192] = 1u; @@ -754,6 +938,7 @@ std::vector pixelNoPerspectiveLocations(bool barycentricEnabled) } } if (barycentricEnabled) Require(perVertex == 2u && perspectiveBarycentrics && linearBarycentrics, "explicit interpolation must preserve both parameter vertices and both barycentric inputs"); + if (!barycentricEnabled) Require(!result.barycentricEmulation.active, "v_interp_p1/p2 of their own I/J pairs did not keep host interpolation"); std::vector result2; for (const auto id : noPerspective) result2.push_back(locations.count(id) != 0 ? locations.at(id) : 0xffffffffu); return result2; @@ -835,6 +1020,15 @@ void ColorPipeBankXorTests() { const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_D_X color base did not split its block address and XOR"); } + queue.context[0x3b8] = 0x4dc60000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xff00u}) { + const auto address = block + pipeBankXor; + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeState(queue).color; + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Z64KBX && color.address == block && color.surfaceAddress == block && color.pipeBankXor == pipeBankXor, "a SW_64KB_Z_X color base did not split its block address and XOR"); + } + queue.context[0x3b8] = 0x0dc68000u; for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) { queue.context[0x318] = static_cast((block + columnBits) >> 8u); queue.context[0x390] = static_cast((block + columnBits) >> 40u); @@ -964,6 +1158,19 @@ void PixelInputLayoutTests() { const auto noPerspective = pixelNoPerspectiveLocations(false); Require(noPerspective.size() == 1 && noPerspective[0] == 1u, "only the parameter interpolated through the linear pair must be NoPerspective"); Require(pixelNoPerspectiveLocations(true).empty(), "explicit interpolation must not interpolate parameter arrays a second time"); + for (const auto inputs : {0x11u, 0x12u, 0x21u}) { + const auto sampleNoPerspective = pixelNoPerspectiveLocations(false, inputs); + Require(sampleNoPerspective.size() == 1 && sampleNoPerspective[0] == 1u, "a parameter interpolated through a sample I/J pair lost its interpolation mode: inputs " + std::to_string(inputs)); + Require(pixelNoPerspectiveLocations(true, inputs).empty(), "explicit interpolation through a sample I/J pair interpolated parameter arrays a second time: inputs " + std::to_string(inputs)); + } + for (const auto source : {0u, 1u}) { + const auto read = pixelBuiltinsRead(0x3u, 0x3u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordKHR) && !readsBuiltin(read, spv::BuiltInFragCoord), "PERSP_SAMPLE does not hold the perspective barycentrics: v" + std::to_string(source)); + } + for (const auto source : {2u, 3u}) { + const auto read = pixelBuiltinsRead(0x12u, 0x12u, source); + Require(readsBuiltin(read, spv::BuiltInBaryCoordNoPerspKHR) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "LINEAR_SAMPLE does not hold the linear barycentrics: v" + std::to_string(source)); + } auto read = pixelBuiltinsRead(0x106u, 0x106u, 4u); Require(readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "POS_X is not in v4 after the center and centroid pairs"); read = pixelBuiltinsRead(0x326u, 0x326u, 5u); @@ -976,6 +1183,16 @@ void PixelInputLayoutTests() { Require(!readsBuiltin(read, spv::BuiltInFragCoord) && !readsBuiltin(read, spv::BuiltInBaryCoordKHR), "an ADDR-only centroid pair was loaded"); } +void BlendBypassTests() { + auto queue = makeState(); + for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; + queue.context[0x1e0] = (1u << 30u) | 4u | (5u << 8u); + Require(AgcDriver::Graphics::DecodeState(queue).blends.at(0).blendEnable, "enabled blending was not decoded"); + queue.context[0x31c] |= 0x10000u; + const auto bypassed = AgcDriver::Graphics::DecodeState(queue); + Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends"); +} + void OpaqueDestinationAlphaTests() { auto queue = makeState(); for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; @@ -1192,6 +1409,28 @@ void DepthStencilTests() { for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; queue.context[0x005] = 0x300; queue.context[0x01e] = 0x1; + { + AgcDriver::QueueState unwritten; + unwritten.shader = queue.shader; + unwritten.userConfig = queue.userConfig; + queue.context[0x200] = 0x00700791; + for (int reset = 0; reset < 2; ++reset) { + queue.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { unwritten.context[offset] = value; }); + const auto state = AgcDriver::Graphics::DecodeState(unwritten); + Require(state.stencilTest && state.depth, "unwritten stencil registers rejected a stencil draw"); + for (const auto face : {state.stencilFront, state.stencilBack}) { + Require(face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP && face.compareOp == VK_COMPARE_OP_ALWAYS, "unwritten stencil operations changed"); + Require(face.compareMask == 0 && face.writeMask == 0 && face.reference == 0, "unwritten stencil reference or masks changed"); + } + Require(AgcDriver::Graphics::DrawRejection(unwritten, false).empty(), "precheck rejected unwritten stencil registers"); + unwritten.context[0x10b] = 0x00070070; + unwritten.context[0x10c] = unwritten.context[0x10d] = 0x01ffff09; + const auto explicitState = AgcDriver::Graphics::DecodeState(unwritten); + for (const auto face : {explicitState.stencilFront, explicitState.stencilBack}) Require(face.passOp == VK_STENCIL_OP_INVERT && face.compareMask == 0xff && face.writeMask == 0xff && face.reference == 9, "explicit stencil state was replaced by defaults"); + unwritten.ClearContext(); + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) Require(unwritten.context.at(offset) == 0, "context clear did not reset stencil state"); + } + } queue.context[0x10b] = 0x00050050; queue.context[0x10c] = 0x01ffff01; queue.context[0x10d] = 0x01000001; @@ -1382,6 +1621,17 @@ void DepthStencilTests() { queue.context[0x207] = (1u << 18u) | (1u << 21u) | (1u << 24u); const auto layered = AgcDriver::Graphics::DecodeState(queue); Require(layered.color.depthSlice == 1u && layered.color.layers == 3u && layered.renderLayers == 3u && layered.layerExports == (1u << 18u), "a color view of three 3D depth slices did not render three layers from the vertex layer export"); + { + auto pointSize = queue; + pointSize.context[0x1b3] = pointSize.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(pointSize, false).empty(), "layered volume test state failed the draw precheck"); + pointSize.context[0x207] |= 1u << 16u; + const auto rejection = AgcDriver::Graphics::DrawRejection(pointSize, false); + Require(rejection.empty(), "inactive point size rejected a layered volume draw: " + rejection); + const auto decoded = AgcDriver::Graphics::DecodeState(pointSize); + Require(decoded.color.depthSlice == layered.color.depthSlice && decoded.color.layers == layered.color.layers && decoded.renderLayers == layered.renderLayers && decoded.layerExports == layered.layerExports, "inactive point size changed layered volume routing"); + Require(decoded.paClVsOutCntl == pointSize.context[0x207], "inactive point size discarded the layered export state"); + } queue.context[0x31b] = 1u | (8u << 13u); Require(AgcDriver::Graphics::DecodeState(queue).renderLayers == 3u, "a color view past the 3D surface was not clamped to its last slice"); queue.context[0x207] = 0; @@ -1689,6 +1939,15 @@ void metadataPassTests() { queue.context[0x0] = 0; queue.context[0x200] = 0x70; Require(DecodeColorMetadataPass(queue).has_value(), "an always-pass depth function without a test refused the pass"); + auto bound = queue; + bound.context[0x010] = 0x3; + bound.context[0x011] = 0x1; + bound.context[0x200] = 0x72; + Require(DecodeColorMetadataPass(bound).has_value(), "an always-pass depth test without writes refused the pass"); + bound.context[0x200] = 0x76; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); + bound.context[0x200] = 0x7a; + expectFailure([&] { DecodeColorMetadataPass(bound); }, "depth or stencil work"); queue.context[0x91] = 0x40020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "over part of a color target"); queue.context[0x91] = 0x40040; @@ -1854,6 +2113,16 @@ void cmaskTests() { auto mipmapped = queue; mipmapped.context[0x3b0] |= 1u << 28u; expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); + constexpr std::size_t blockBytes = 65536u; + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + auto higherMip = queue; + higherMip.context[0x318] = static_cast(mipBase >> 8u); + higherMip.context[0x390] = static_cast(mipBase >> 40u); + higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + higherMip.context[0x31b] = 1u << 26u; + const auto higherTarget = DecodeColorBuffer(higherMip.context, 0); + Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK"); auto unaddressed = queue; unaddressed.context[0x31f] = 0; unaddressed.context[0x398] = 0; @@ -2414,14 +2683,86 @@ void DepthClipTests() { } } -void AlphaToCoverageTests() { +void UnditheredAlphaToCoverageTests() { auto queue = makeState(); + queue.context[0x203] = 0x8010; + queue.context[0x2dc] = 0xaa01; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + for (const auto samples : {1u, 8u}) { + if (samples == 8) { + queue.context[0x201] = 0x00133303; + queue.context[0x2f8] = 0x0030e003; + queue.context[0x292] = 3; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + } + for (const auto disabled : {false, true}) { + queue.context[0x203] = 0x8010u | (disabled ? 0x800u : 0u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.alphaToCoverage && !state.ditheredAlphaToCoverage && state.samples == samples, "the existing undithered alpha-to-coverage state changed"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered alpha-to-coverage state"); + } + } +} + +void AlphaToCoverageTests() { + UnditheredAlphaToCoverageTests(); + auto dithered = makeState(); + dithered.context[0x203] = 0x8010; + dithered.context[0x201] = 0x00133303; + dithered.context[0x2dc] = 0x18701; + dithered.context[0x2f8] = 0x0030e003; + dithered.context[0x292] = 3; + dithered.context[0x31d] = 0x1b000; + dithered.context[0x1b3] = 2; + dithered.context[0x1b4] = 2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + dithered.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + dithered.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + const auto rejection = AgcDriver::Graphics::DrawRejection(dithered, false); + Require(rejection.empty(), "eight-sample default dither refused: " + rejection); + const auto ditheredState = AgcDriver::Graphics::DecodeState(dithered); + Require(ditheredState.alphaToCoverage && ditheredState.samples == 8, "default dither did not enable eight-sample coverage"); + Require(ditheredState.ditheredAlphaToCoverage, "default dither did not reach the shader state"); + for (const auto eqaa : {0x00133333u, 0x00132303u, 0x00133203u, 0x00333303u}) { + dithered.context[0x201] = eqaa; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "DB_EQAA"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("DB_EQAA") != std::string::npos, "the precheck accepted unmatched dithered coverage samples"); + } + dithered.context[0x201] = 0x00133303; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "custom sample locations"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("custom sample locations") != std::string::npos, "the precheck accepted unmeasured dithered sample locations"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) dithered.context[0x2fe + pixel * 4u] ^= 1u; + for (const auto format : {3u, 5u, 6u}) { + dithered.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "FP16_ABGR or 32_ABGR"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).find("FP16_ABGR or 32_ABGR") != std::string::npos, "the precheck accepted an unmeasured dithered export format"); + } + dithered.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(dithered).ditheredAlphaToCoverage, "FP16 default dither was refused"); + dithered.context[0x203] |= 0x800u; + Require(!AgcDriver::Graphics::DecodeState(dithered).alphaToCoverage, "the shader disable override was ignored"); + Require(AgcDriver::Graphics::DrawRejection(dithered, false).empty(), "the precheck ignored the shader disable override"); + dithered.context[0x2dc] |= 2u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(dithered); }, "alpha-to-coverage"); + auto queue = makeState(); + queue.context[0x203] = 0; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; Require(!AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage decoded while disabled"); queue.context[0x2dc] = 0xaa01; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected undithered alpha-to-coverage"); + queue.context[0x203] |= 0x800u; + Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "the existing undithered single-sample state changed under the shader disable override"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck changed the existing undithered single-sample state under the shader disable override"); + queue.context[0x203] &= ~0x800u; for (const auto control : {0x8701u, 0x1aa01u, 0xaa03u}) { queue.context[0x2dc] = control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alpha-to-coverage"); @@ -2429,14 +2770,23 @@ void AlphaToCoverageTests() { } queue.context[0x2dc] = 0xaa01; const std::uint32_t exportFormat = queue.context.at(0x1c5); - for (const auto format : {4u, 5u, 6u, 9u}) { + for (const auto format : {3u, 4u, 5u, 6u, 9u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "alpha-to-coverage rejected an MRT0 export format with a floating-point alpha"); } - for (const auto format : {1u, 2u, 3u}) { + for (const auto format : {1u, 2u}) { queue.context[0x1c5] = (exportFormat & ~0xfu) | format; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); } + const auto targetMask = queue.context.at(0x8e); + queue.context[0x8e] = 0; + queue.context[0x1c5] = (exportFormat & ~0xfu) | 3u; + const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); + Require(depthOnly.alphaToCoverage && depthOnly.colors.empty(), "alpha-to-coverage without a written color target and a 32_AR MRT0 export was refused or kept a target"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected alpha-to-coverage without a written color target"); + queue.context[0x1c5] = exportFormat & ~0xfu; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "floating-point alpha"); + queue.context[0x8e] = targetMask; queue.context[0x1c5] = exportFormat; } @@ -2456,6 +2806,7 @@ void InitialContextTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_VTE_CNTL=0x"); } queue.context[0x206] = 0x43f; + queue.context[0x203] &= ~0x800u; queue.context[0x2dc] |= 1; Require(AgcDriver::Graphics::DecodeState(queue).alphaToCoverage, "undithered alpha-to-coverage was not decoded"); queue.context.erase(0x2dc); @@ -2486,6 +2837,8 @@ struct MockVulkan { VkDeviceSize allocatedBytes = 0; std::optional memoryLimit; std::uint64_t allocationAttempts = 0; + std::uint32_t bufferMemoryTypeBits = 1; + std::vector allocationTypes; std::vector layoutBindings; VkDescriptorSetLayoutCreateFlags layoutFlags = 0; std::vector layoutBindingFlags; @@ -2538,11 +2891,12 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - *requirements = {mock.bufferSizes.at(buffer), 1, 1}; + *requirements = {mock.bufferSizes.at(buffer), 1, mock.bufferMemoryTypeBits}; } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { ++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; *memory = makeHandle(); mock.memories[*memory] = std::vector(info->allocationSize); @@ -2828,7 +3182,72 @@ AgcDriver::Graphics::Context mockContext() { return context; } +void addressSpaceTeardownTests() { +#ifdef _WIN32 + _putenv_s("APS5_NO_LEASE_MIRROR", "1"); +#else + setenv("APS5_NO_LEASE_MIRROR", "1", 1); +#endif + auto context = mockContext(); + context.device = makeHandle(); + context.bufferDeviceAddress = true; + context.limits.maxStorageBufferRange = 1u << 27; + void* block = GuestHeap::GuestHeapAlignForGpu_nid_postfix(16, 64); + const auto before = AgcDriver::Graphics::LeaseCounters().cacheDrops; + { + AgcDriver::Graphics::GuestBufferMemory memory(context); + memory.AcquireRegistered(); + Require(memory.HoldsLease(), "address-based build holds no lease before device teardown"); + memory.Upload(true); + memory.WriteBack(); + Require(!memory.HoldsLease(), "completed address-based build kept its own lease"); + } + AgcDriver::Graphics::ClearImageMirrors(context.device); + GuestHeap::GuestHeapFree_nid_postfix(block); + Require(AgcDriver::Graphics::LeaseCounters().cacheDrops == before, "device teardown left the cached address space pinning guest allocations"); + Require(mock.live == 0, "address-space teardown test leaked Vulkan objects"); +} + using Role = ShaderRecompiler::DescriptorRole; + +void bufferBudgetPlacementTests() { + using namespace AgcDriver::Graphics; + for (const std::size_t bytes : {std::size_t{512}, std::size_t{3} << 20u}) { + for (const bool fail : {false, true}) { + mock = MockVulkan{}; + mock.bufferMemoryTypeBits = 3; + if (fail) mock.memoryLimit = 0; + { + auto context = mockContext(); + constexpr auto coherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + context.memory.memoryTypeCount = 2; + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4u << 20u, 0}; + context.memory.memoryHeaps[1] = {8u << 20u, 0}; + context.memory.memoryTypes[0] = {coherent | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, 0}; + context.memory.memoryTypes[1] = {coherent, 1}; + context.physical = reinterpret_cast(std::uintptr_t{1}); + context.memoryProperties2 = [](VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + reported->heapBudget[0] = 1u << 20u; + reported->heapUsage[0] = reported->heapBudget[0] - 1024u; + reported->heapBudget[1] = 8u << 20u; + }; + const auto allocate = [&] { + Buffer buffer(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Require(buffer.Mapped() && buffer.Bytes().size() == bytes, "a budget-selected buffer lost its mapping or size"); + buffer.Bytes().front() = std::byte{0x5a}; + Require(mockBufferMemory(buffer.Handle()).front() == std::byte{0x5a}, "a budget-selected buffer bound the wrong allocation"); + }; + if (fail) expectFailure(allocate, "Vulkan result -2"); + else allocate(); + Require(mock.allocationTypes == std::vector{1} && mock.allocationAttempts == 1, "a coherent buffer did not select the other heap before its single allocation attempt"); + if (!fail) Require(mock.allocatedBytes == (bytes == 512 ? BufferPool::SlabBlockBytes(512) : bytes), "a coherent buffer budget check lost its full allocation extent"); + } + Require(mock.live == 0, "a budget-selected buffer leaked Vulkan objects"); + } + } +} using Kind = ShaderRecompiler::DescriptorKind; alignas(16) std::array guestFirst{0x11111111, 0x22222222, 0x33333333, 0x44444444}; @@ -4031,6 +4450,9 @@ void rectListTests() { const auto complete = drawFailure(3, indexed); for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); } + Require(drawFailure(0, true).empty(), "an indexed draw without indices was not skipped"); + const AgcDriver::Pm4::DrawParameters noInstances{reinterpret_cast(indices.data()), 3, 2, 0, 0, true}; + Draw(context, state, noInstances, {}); } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { @@ -4490,7 +4912,7 @@ void aliasedBufferViewTests() { Require(declared == views, "the pixel shader declared the guest buffers as" + names + ", not as the expected " + std::to_string(views.size()) + " aliased variables"); const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); - auto unaliased = pixel.spirv.Words(); + auto unaliased = pixel.spirv.Copy(); for (std::size_t at = 5; at < unaliased.size() && (unaliased[at] >> 16u) != 0; at += unaliased[at] >> 16u) { if (static_cast(unaliased[at] & 0xffffu) == spv::OpDecorate && unaliased[at + 2] == spv::DecorationAliased) unaliased[at + 2] = spv::DecorationRestrict; } @@ -4840,7 +5262,7 @@ void unorm10_11_11TargetTests() { const auto packed = ShaderRecompiler::Recompile(request); const auto unpacked = ShaderRecompiler::Recompile(plain); Require(packed.variantId == unpacked.variantId && packed.variantId == ShaderRecompiler::GetPreparedArtifact(*prepared).variantId, "the packed draw did not reuse the registered artifact"); - Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && packed.spirv.Words() != unpacked.spirv.Words(), "the export packing did not specialize the module"); + Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && !(packed.spirv == unpacked.spirv), "the export packing did not specialize the module"); Require(ShaderRecompiler::Recompile(request).PipelineVariantId() == packed.PipelineVariantId(), "the packed specialization was not reused"); ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); @@ -5521,6 +5943,79 @@ void parallelCompareTests() { CompareSpansFrom({}, none, 0); } +void ResourceContentKeyTests(bool benchmark) { + using AgcDriver::Graphics::ShaderResources; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + for (const auto count : {0u, 1u, 31u, 32u, 33u, 16384u}) { + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::GuestImages; + images.count = count; + images.guestDescriptor.resize(static_cast(count) * 8u); + for (std::size_t i = 0; i < images.guestDescriptor.size(); ++i) images.guestDescriptor[i] = static_cast(i * 1664525u + 1013904223u); + for (auto* bits : {&images.imageWritten, &images.samplerDepthCompare, &images.imageDepthCompare, &images.imageAtomic, &images.samplerUnnormalized, &images.samplerIntegerBorder, &images.imageUnnormalized, &images.imageUnnormalizedBaseLevel, &images.bufferWritten}) { + bits->resize(count); + for (std::size_t i = 0; i < bits->size(); ++i) (*bits)[i] = i % 3u == 0; + } + images.imageSamplers.resize(count, 7u); + program.bindings.push_back(std::move(images)); + ShaderRecompiler::DescriptorBinding data{}; + data.kind = DescriptorKind::StorageBuffer; + data.role = DescriptorRole::ShaderData; + data.count = 1; + data.guestDescriptor.resize(257u, 0x12345678u); + program.bindings.push_back(std::move(data)); + ShaderRecompiler::DescriptorBinding buffers{}; + buffers.kind = DescriptorKind::StorageBuffer; + buffers.role = DescriptorRole::GuestBuffers; + buffers.count = 1; + buffers.guestDescriptor = {0x1000u, 0x10000u, 4u, 0x1234u}; + buffers.bufferWritten = {false}; + program.bindings.push_back(std::move(buffers)); + const AgcDriver::Graphics::CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + for (const bool dataWords : {false, true}) for (const bool movable : {false, true}) { + const auto original = ShaderResources::ContentKey(shader, dataWords, movable); + program.bindings[1].guestDescriptor.back() ^= 1u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == (dataWords && !movable), "content key changed its data-word policy"); + program.bindings[1].guestDescriptor.back() ^= 1u; + program.bindings[2].guestDescriptor[0] += 0x1000u; + Require((ShaderResources::ContentKey(shader, dataWords, movable) != original) == !movable, "content key changed its movable-buffer policy"); + program.bindings[2].guestDescriptor[0] -= 0x1000u; + program.bindings[2].bufferWritten[0] = true; + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the written-buffer mask"); + program.bindings[2].bufferWritten[0] = false; + if (count != 0) { + for (auto* bits : {&program.bindings[0].imageWritten, &program.bindings[0].samplerDepthCompare, &program.bindings[0].imageDepthCompare, &program.bindings[0].imageAtomic, &program.bindings[0].samplerUnnormalized, &program.bindings[0].samplerIntegerBorder, &program.bindings[0].imageUnnormalized, &program.bindings[0].imageUnnormalizedBaseLevel}) { + bits->back().flip(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last packed flag"); + bits->back().flip(); + } + ++program.bindings[0].imageSamplers.back(); + Require(ShaderResources::ContentKey(shader, dataWords, movable) != original, "content key ignored the last sampler association"); + --program.bindings[0].imageSamplers.back(); + } + Require(original.capacity() == original.size(), "content key reserved a different word count than it serialized"); + std::uint64_t hash = 14695981039346656037ull; + for (const auto word : original) hash = (hash ^ word) * 1099511628211ull; + std::cout << "content-key count=" << count << " data=" << dataWords << " movable=" << movable << " words=" << original.size() << " capacity=" << original.capacity() << " hash=" << hash; + if (benchmark) { + constexpr std::size_t repeats = 256; + std::uint64_t checksum = 0; + const auto begin = std::chrono::steady_clock::now(); + for (std::size_t i = 0; i < repeats; ++i) { + const auto key = ShaderResources::ContentKey(shader, dataWords, movable); + checksum += key.size() + key.back(); + } + const auto elapsed = std::chrono::duration(std::chrono::steady_clock::now() - begin).count(); + std::cout << " us/key=" << elapsed / repeats << " checksum=" << checksum; + } + std::cout << '\n'; + } + } +} + int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "200"); @@ -5543,11 +6038,45 @@ int main(int argc, char** argv) { std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n'; return passed ? 0 : 1; } + if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") { + addressSpaceTeardownTests(); + std::cout << "address-space device teardown releases cached guest allocations\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--buffer-budget-placement") { + bufferBudgetPlacementTests(); + return 0; + } + { + AgcDriver::Graphics::PipelineCacheSynchronization synchronization; + auto pipelineUse = synchronization.LockForPipelineUse(); + bool snapshotAcquired = false; + std::thread snapshot([&] { snapshotAcquired = synchronization.TryLockForSnapshot().owns_lock(); }); + snapshot.join(); + Require(!snapshotAcquired, "pipeline cache snapshot overlapped pipeline creation"); + pipelineUse.unlock(); + Require(synchronization.TryLockForSnapshot().owns_lock(), "pipeline cache snapshot stayed blocked after pipeline creation finished"); + } + if (argc == 2 && std::string_view(argv[1]) == "--undithered-alpha-to-coverage-state") { + UnditheredAlphaToCoverageTests(); + std::cout << "Existing undithered alpha-to-coverage states passed\n"; + return 0; + } + if (argc == 2 && (std::string_view(argv[1]) == "--resource-content-key" || std::string_view(argv[1]) == "--benchmark-resource-content-key")) { + ResourceContentKeyTests(std::string_view(argv[1]) == "--benchmark-resource-content-key"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--alpha-to-coverage-state") { + MsaaEnableSingleSampleTests(); + AlphaToCoverageTests(); + std::cout << "Alpha-to-coverage state and refusal tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; @@ -5589,6 +6118,7 @@ int main(int argc, char** argv) { AgcDriver::Graphics::ValidateDepthBounds(unrestricted, bounded); } RunGuestLeaseWaitTests(); + ResourceContentKeyTests(false); stateTests(); pipelineLibrarySetTests(); hdrComponentSwapTests(); @@ -5615,6 +6145,7 @@ int main(int argc, char** argv) { DisabledColorTests(); OpaqueDestinationAlphaTests(); Color1555TargetTests(); + BlendBypassTests(); CompactedExportTests(); ReversedComponentOrderTests(); ShadersWriteMemoryTests(); @@ -5627,8 +6158,10 @@ int main(int argc, char** argv) { MsaaEnableSingleSampleTests(); clipDistanceTests(); framebufferLayerTests(); + UnwrittenSampleLocationTests(); ShaderStageTests(); NggVertexWorkgroupTests(); + inactiveVertexPointSizeTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); @@ -5644,6 +6177,7 @@ int main(int argc, char** argv) { resourceTests(); descriptorSnapshotTests(); descriptorCacheTests(); + bufferBudgetPlacementTests(); misalignedShaderDataTests(); debugBranchTests(); textureCacheBudgetTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index a605c6cd6a..bd5e36614f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -446,6 +446,24 @@ void RunGuestTextureResourceTests() { Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k2DArray), "3D shape must never match a guest dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::kCube), "3D shape must never match a guest dimension"); + const auto layered = [](TextureDimension dimension, std::uint32_t lastLayer, std::uint32_t baseArray = 0) { + GuestTextureResource resource{}; + resource.dimension = dimension; + resource.depthOrLastArray = lastLayer; + resource.baseArray = baseArray; + return resource; + }; + const auto twoSlices = layered(TextureDimension::k2DArray, 1); + Require(ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0)), "a 2D texture must view the first layer of a 2D array color surface (slice-0 view with MIP0_DEPTH 1)"); + Require(ArrayLayersServeView(twoSlices, twoSlices), "a 2D array must view the same 2D array"); + Require(ArrayLayersServeView(layered(TextureDimension::k2DArray, 3), twoSlices), "a 2D array must view the first layers of a larger 2D array"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2DArray, 3)), "a 2D array view must not reach past the surface's layers"); + Require(!ArrayLayersServeView(twoSlices, layered(TextureDimension::k2D, 0, 1)), "a 2D view of a later layer must not be served by the array's first layer"); + Require(ArrayLayersServeView(layered(TextureDimension::k2D, 0), layered(TextureDimension::k2D, 0)), "a 2D texture must view the same 2D surface"); + Require(!ArrayLayersServeView(layered(TextureDimension::k2D, 0), twoSlices), "a 2D surface must not serve a 2D array view"); + Require(!ArrayLayersServeView(layered(TextureDimension::k3D, 1), layered(TextureDimension::k2D, 0)), "a 3D surface's first slice must not serve a 2D texture"); + Require(!ArrayLayersServeView(layered(TextureDimension::kCube, 5), layered(TextureDimension::k2D, 0)), "a cube's first face must not serve a 2D texture"); + Fields streamed = base; streamed.minLodWarn = 0xabc; streamed.mipStatsCntEn = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp new file mode 100644 index 0000000000..a6d05d4dc7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifndef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return 77; +} +#else +using namespace AgcDriver::Graphics; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +constexpr std::uint64_t Alignment = 65536; +constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; + +struct Bind { + VkDeviceSize resourceOffset; + VkDeviceSize size; + VkDeviceMemory memory; + VkDeviceSize memoryOffset; +}; + +struct MockDevice { + std::uintptr_t next = 0x1000; + std::set buffers; + std::set sparse; + std::map sizes; + std::map memories; + std::vector binds; + VkResult bindResult = VK_SUCCESS; + std::uint64_t nextAddress = 0x500000000ull; +}; + +MockDevice mock; + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { + *fence = reinterpret_cast(mock.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {} + +VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryHostPointerProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, const void*, VkMemoryHostPointerPropertiesEXT*) { + return VK_ERROR_INVALID_EXTERNAL_HANDLE; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockGetMemoryFdProperties(VkDevice, VkExternalMemoryHandleTypeFlagBits, int, VkMemoryFdPropertiesKHR* properties) { + properties->memoryTypeBits = 1; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = reinterpret_cast(mock.next++); + mock.buffers.insert(*buffer); + if ((info->flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0) mock.sparse.insert(*buffer); + mock.sizes[*buffer] = info->size; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + mock.buffers.erase(buffer); +} + +VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { + requirements->size = (mock.sizes.at(buffer) + Alignment - 1) / Alignment * Alignment; + requirements->alignment = Alignment; + requirements->memoryTypeBits = 1; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + *memory = reinterpret_cast(mock.next++); + mock.memories[*memory] = info->allocationSize; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + mock.memories.erase(memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockQueueBindSparse(VkQueue, std::uint32_t count, const VkBindSparseInfo* infos, VkFence) { + if (mock.bindResult != VK_SUCCESS) return mock.bindResult; + for (std::uint32_t i = 0; i < count; ++i) { + for (std::uint32_t b = 0; b < infos[i].bufferBindCount; ++b) { + const auto& buffer = infos[i].pBufferBinds[b]; + for (std::uint32_t m = 0; m < buffer.bindCount; ++m) mock.binds.push_back({buffer.pBinds[m].resourceOffset, buffer.pBinds[m].size, buffer.pBinds[m].memory, buffer.pBinds[m].memoryOffset}); + } + } + return VK_SUCCESS; +} + +VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VkBufferDeviceAddressInfo* info) { + const auto address = mock.nextAddress; + mock.nextAddress += (mock.sizes.at(info->buffer) + Alignment - 1) / Alignment * Alignment; + return address; +} + +PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { + static const std::map table{ + {"vkCreateFence", reinterpret_cast(mockCreateFence)}, + {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, + {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, + {"vkGetMemoryHostPointerPropertiesEXT", reinterpret_cast(mockGetMemoryHostPointerProperties)}, + {"vkGetMemoryFdPropertiesKHR", reinterpret_cast(mockGetMemoryFdProperties)}, + {"vkCreateBuffer", reinterpret_cast(mockCreateBuffer)}, + {"vkDestroyBuffer", reinterpret_cast(mockDestroyBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(mockGetBufferMemoryRequirements)}, + {"vkAllocateMemory", reinterpret_cast(mockAllocateMemory)}, + {"vkFreeMemory", reinterpret_cast(mockFreeMemory)}, + {"vkBindBufferMemory", reinterpret_cast(mockBindBufferMemory)}, + {"vkQueueBindSparse", reinterpret_cast(mockQueueBindSparse)}, + {"vkGetBufferDeviceAddressKHR", reinterpret_cast(mockGetBufferDeviceAddress)}, + }; + const auto it = table.find(name); + return it == table.end() ? nullptr : it->second; +} + +Context mockContext() { + static const auto device = reinterpret_cast(mock.next++); + static const auto queue = reinterpret_cast(mock.next++); + Context context{}; + context.device = device; + context.queue = queue; + context.deviceProc = mockProc; + context.hostImportAlignment = Alignment; + context.dmaBufImport = true; + context.sparseBinding = true; + return context; +} + +struct Backing { + int file = -1; + std::uint8_t* base = nullptr; + std::uint64_t bytes = 0; + std::uint64_t fileOffset = 0; + void* reservation = nullptr; + std::uint64_t reserved = 0; +}; + +std::vector backings; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + for (const auto* backing : backings) { + const auto begin = reinterpret_cast(backing->base); + if (address < begin || address + bytes > begin + backing->bytes) continue; + *file = dup(backing->file); + *offset = backing->fileOffset + (address - begin); + return *file >= 0; + } + return false; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +struct SharedRange { + Backing backing; + + SharedRange(std::uint64_t bytes, std::uint64_t fileOffset, bool sealed) { + backing.bytes = bytes; + backing.fileOffset = fileOffset; + backing.file = memfd_create("anyps5-sparse-import-mock", MFD_CLOEXEC | MFD_ALLOW_SEALING); + const auto fileBytes = (fileOffset + bytes + ChunkBytes - 1) / ChunkBytes * ChunkBytes; + if (backing.file < 0 || ftruncate(backing.file, static_cast(fileBytes)) != 0) throw std::runtime_error("cannot make the backing file"); + if (sealed && fcntl(backing.file, F_ADD_SEALS, F_SEAL_SHRINK) != 0) throw std::runtime_error("cannot seal the backing file"); + backing.reserved = bytes + Alignment; + backing.reservation = mmap(nullptr, backing.reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (backing.reservation == MAP_FAILED) throw std::runtime_error("cannot reserve the guest range"); + const auto aligned = (reinterpret_cast(backing.reservation) + Alignment - 1) / Alignment * Alignment; + void* mapped = mmap(reinterpret_cast(aligned), bytes, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, backing.file, static_cast(fileOffset)); + if (mapped == MAP_FAILED) throw std::runtime_error("cannot map the backing file"); + backing.base = static_cast(mapped); + backings.push_back(&backing); + GuestAllocations::Mutation mutation; + mutation.Add(backing.base, static_cast(bytes), true, true, true); + } + + std::uint64_t Address() const { + return reinterpret_cast(backing.base); + } +}; + +std::uint64_t RangeBytes() { + return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u); +} + +void ImportsALargeRangeAsOneSparseBuffer() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 0, true); + const auto context = mockContext(); + mock.binds.clear(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import != nullptr, "a shared range larger than udmabuf's size limit was not imported"); + if (import == nullptr) return; + Expect(import->base == range.Address() && import->bytes == bytes && import->dmaBuf, "the import does not cover the whole range as a dma-buf import"); + Expect(mock.sparse.contains(import->buffer), "the range was not imported as one sparse buffer"); + const auto chunks = (bytes + ChunkBytes - 1) / ChunkBytes; + Expect(mock.binds.size() == chunks, "the sparse buffer is not bound to one chunk import per 64 MiB of the range"); + VkDeviceSize resource = 0; + std::set memories; + for (const auto& bind : mock.binds) { + Expect(bind.resourceOffset == resource && bind.memoryOffset == 0 && bind.size == std::min(ChunkBytes, bytes - resource), "a bind does not map its chunk import to the matching part of the range"); + Expect(mock.memories.contains(bind.memory) && mock.memories.at(bind.memory) == ChunkBytes, "a bind does not use a 64 MiB chunk import"); + memories.insert(bind.memory); + resource += bind.size; + } + Expect(memories.size() == chunks, "two parts of the range are bound to the same chunk import"); +} + +void UnalignedOffsetIsDecidedBeforeImporting() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 4096, true); + const auto context = mockContext(); + mock.binds.clear(); + const auto memories = mock.memories.size(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import == nullptr, "a range whose backing offset is not on the sparse block size was imported above udmabuf's size limit"); + Expect(mock.memories.size() == memories && mock.binds.empty(), "a chunk was imported or bound for a range that does not take the sparse path"); +} + +void FailureAfterTheDecisionThrows(bool sealed, VkResult bind, const char* step) { + const auto bytes = RangeBytes(); + SharedRange range(bytes, 0, sealed); + const auto context = mockContext(); + mock.bindResult = bind; + const auto live = mock.buffers.size(); + std::string thrown; + try { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + HostImportFor(context, range.Address(), static_cast(bytes)); + } catch (const std::runtime_error& error) { + thrown = error.what(); + } + mock.bindResult = VK_SUCCESS; + Expect(thrown.find("sparse dma-buf import") != std::string::npos && thrown.find(step) != std::string::npos, std::string("a failure at ") + step + " after the sparse path was chosen did not throw (got \"" + thrown + "\")"); + Expect(mock.buffers.size() == live, std::string("the sparse buffer outlived the failure at ") + step); +} + +} + +int main() { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); + return 77; + } + close(udmabuf); + setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); + GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); + try { + ImportsALargeRangeAsOneSparseBuffer(); + UnalignedOffsetIsDecidedBeforeImporting(); + FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE"); + FailureAfterTheDecisionThrows(true, VK_ERROR_DEVICE_LOST, "vkQueueBindSparse"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (failures != 0) { + std::fprintf(stderr, "%d sparse dma-buf import checks failed\n", failures); + return 1; + } + std::printf("host import sparse dma-buf tests passed\n"); + return 0; +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp index c7843ab418..b0dce8e47b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp @@ -39,6 +39,14 @@ void checkLiveStack(std::uintptr_t stackBottom, std::uintptr_t stackTop, std::ui void RunLiveStackAccessTests() { checkLiveStack(0, 0, 0); +#if defined(_WIN32) + for (const auto address : {std::uintptr_t{0xf00000000000ull}, std::uintptr_t{0x7fffffffffffull}}) { + Require(!GuestMemory::Accessible(reinterpret_cast(address), 4), + "an address above the user address space is reported accessible"); + const auto committed = GuestMemory::CommittedRanges(address, 4, false); + Require(committed.empty(), "an address above the user address space reports committed bytes"); + } +#endif #if defined(__linux__) const auto page = static_cast(sysconf(_SC_PAGESIZE)); const std::size_t stackBytes = 256 * 1024; diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 29891d5980..ac2fff1057 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,13 @@ alignas(256) constexpr auto Wave32GeometryCode = [] { return code; }(); +alignas(256) constexpr std::array FastLaunchCode{ + 0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010, + 0x80020601, 0xb07c1003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302, + 0xf800020f, 0x09080706, 0x7d840080, 0xbefe036a, 0x7e1402ff, 0x00200400, 0xf8000941, 0x0000000a, + 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -132,9 +140,11 @@ struct MeshDraw { bool restart = false; std::uint32_t waveSize = 64u; VkCullModeFlags cull = VK_CULL_MODE_NONE; + std::span code = {}; + std::uint32_t stagesRegister = 0x20u; }; -bool ReadsNumWorkgroups(const std::vector& words) { +bool ReadsNumWorkgroups(std::span words) { constexpr std::uint32_t OpDecorate = 71; constexpr std::uint32_t OpAccessChain = 65; constexpr std::uint32_t OpLoad = 61; @@ -160,7 +170,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw); std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); - const auto& code = setup.waveSize == 32u ? Wave32GeometryCode : GeometryCode; + const std::span code = !setup.code.empty() ? setup.code : setup.waveSize == 32u ? std::span(Wave32GeometryCode) : std::span(GeometryCode); const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; ShaderRecompiler::RecompileRequest geometry{ {ShaderStage::Mesh, reinterpret_cast(code.data()), code, 0, {}}, @@ -194,7 +204,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, setup.waveSize, 64, setup.mesh, std::nullopt}; + state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, setup.stagesRegister, setup.waveSize, 64, setup.mesh, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -215,6 +225,7 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { constexpr ShaderRecompiler::MeshConfiguration SmallSubgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; constexpr ShaderRecompiler::MeshConfiguration WideSubgroup{4u, 32u, 96u, 96u, 32u, 128u, 2048u, 0u, 4u}; +constexpr ShaderRecompiler::MeshConfiguration FastLaunchSubgroup{1u, 1u, 1u, 3u, 1u, 32u, 128u, 0u, 4u}; std::size_t FanPixel(float angle, float radius) { const auto x = static_cast((radius * std::cos(angle) + 1.0f) * 0.5f * Width); @@ -344,6 +355,24 @@ int main() { CheckTriangles("non-indexed wave32 triangle list"); } + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles, 0, 1, 0, false}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + CheckTriangles("fast-launch subgroups"); + ClearPixels(); + DrawMesh(device, {FastLaunchSubgroup, {0, Triangles / 2u, 0, 1, 0, false, Triangles / 4u}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + for (std::uint32_t triangle = 0; triangle < Triangles; ++triangle) { + const bool drawn = triangle >= Triangles / 4u && triangle < Triangles / 4u + Triangles / 2u; + Require(PixelIs(CellPixel(triangle), drawn ? TriangleColor(triangle) : Background), "fast-launch subgroups from a start vertex: triangle " + std::to_string(triangle) + " pixel " + PixelText(CellPixel(triangle))); + } + bool fastLaunchIndexedRefused = false; + try { + DrawMesh(device, {FastLaunchSubgroup, {reinterpret_cast(Indices.data()), Triangles, 2, 1, 0, true}, VertexBufferDescriptor(Ordered.data(), static_cast(Ordered.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, FastLaunchCode, 0x482030u}); + } catch (const std::runtime_error& error) { + fastLaunchIndexedRefused = std::string(error.what()).find("indexed fast-launch draws are unsupported") != std::string::npos; + if (!fastLaunchIndexedRefused) throw; + } + Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + constexpr std::size_t recordBlockBytes = 65536; auto* record = static_cast(::operator new(recordBlockBytes, std::align_val_t{65536})); { diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 25c9a62bed..28c5136504 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -153,6 +153,78 @@ void GpuClock() { Check(TraceFormat::GpuSlices(ranges, same, same, origin)[0].begin == origin + microseconds(14), "one calibration does not map at the CPU rate"); } +void IntervalDue() { + Check(!AgcDriver::TraceInterval::Parse(nullptr).has_value(), "an interval was made without a switch"); + for (const auto* text : {"", "0", "-5", "1.5", "a", "5s", "1:2", "99999999999"}) { + bool rejected = false; + try { AgcDriver::TraceInterval::Parse(text); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed interval was accepted"); + } + auto interval = AgcDriver::TraceInterval::Parse("30").value(); + Check(interval.Describe() == "summary_every 30", "interval description changed"); + Check(!interval.Due(milliseconds(29999)) && interval.Due(milliseconds(30000)), "the interval was due at the wrong time"); + interval.Restart(milliseconds(30500)); + Check(interval.Begun() == milliseconds(30500) && !interval.Due(milliseconds(60499)) && interval.Due(milliseconds(60500)), "a restarted interval did not count from its restart"); +} + +void SummaryOnlyThread() { + std::atomic presents{1}; + std::atomic buffering{false}; + TraceThread thread("queue worker 0x0", presents, 0, buffering); + const auto origin = Clock::now(); + thread.Add("Driver.Packet", nullptr, origin, origin + milliseconds(2)); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(3), origin + milliseconds(4)); + presents.store(2); + thread.Add("Driver.Completion", nullptr, origin + milliseconds(5), origin + microseconds(5005)); + auto snapshot = thread.Collect(); + Check(snapshot.slices.empty() && !snapshot.dropped, "a thread that is not buffering kept a slice"); + Check(snapshot.totals.size() == 2 && snapshot.totals[0].count + snapshot.totals[1].count == 3, "a thread that is not buffering missed a total"); + buffering.store(true); + thread.Add("Driver.Packet", nullptr, origin + milliseconds(6), origin + milliseconds(7)); + Check(thread.Collect().slices.empty(), "a zero capacity buffer held a slice"); +} + +void TotalsSince() { + const std::vector before{{"Driver.Packet", nullptr, milliseconds(4), 2}, {"Driver.Present", nullptr, milliseconds(1), 1}}; + const std::vector now{{"Driver.Packet", nullptr, milliseconds(10), 5}, {"Driver.Present", nullptr, milliseconds(1), 1}, {"Driver.Completion", nullptr, milliseconds(3), 2}}; + const auto delta = TraceFormat::Since(now, before); + Check(delta.size() == 2, "a total without a new slice was kept or a new one lost"); + Check(delta[0].length == milliseconds(6) && delta[0].count == 3, "a total was not reduced by the previous interval"); + Check(std::string_view(delta[1].scope) == "Driver.Completion" && delta[1].length == milliseconds(3) && delta[1].count == 2, "a total without a previous value changed"); + Check(TraceFormat::Since(now, {}).size() == 3, "the first interval dropped a total"); +} + +void IntervalSummaryFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = true; + summary.environment = {{"APS5_TRACE_SUMMARY_EVERY", "30"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "summary_every 30"; + summary.firstPresent = 120; + summary.gpuBusyMs = 20; + summary.interval = std::make_pair(30.5, 60.25); + const std::vector threads{ + {"queue worker 0x0", {}, {{"Driver.Packet", nullptr, milliseconds(12), 4}}, false}, + }; + const std::vector presents{origin, origin + milliseconds(16), origin + milliseconds(40)}; + const auto line = TraceFormat::Summary(summary, threads, presents); + const std::string expected = + "{\"commit\":\"abc\",\"dirty\":true,\"env\":{\"APS5_TRACE_SUMMARY_EVERY\":\"30\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"resolution\":\"1920x1080\",\"window\":{\"spec\":\"summary_every 30\",\"first_present\":120,\"ms\":40.000},\"interval_start_s\":30.500,\"interval_end_s\":60.250,\"presents\":2," + "\"frame_ms\":{\"p50\":24.000,\"p90\":24.000,\"p99\":24.000,\"max\":24.000},\"gpu_busy_ms_per_present\":10.000," + "\"worker_busy_pct\":{\"queue worker 0x0\":30.000}," + "\"top_phases_ms_per_frame\":{\"Driver.Packet\":6.000}," + "\"trace_dropped\":false,\"trace\":\"\"}\n"; + Check(line == expected, "interval summary line changed"); +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -210,6 +282,10 @@ int main() { EventsShape(); SummaryFields(); GpuClock(); + IntervalDue(); + SummaryOnlyThread(); + TotalsSince(); + IntervalSummaryFields(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 490ba22a37..c2012bbc67 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -66,7 +66,7 @@ void testCatalog() { const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]); if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str()); } - check(values.size() == 55, "reference opcode catalog is incomplete"); + check(values.size() == 56, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); const std::array, 11> custom{{ {5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"}, @@ -168,6 +168,9 @@ void testRegisterFile() { void testContextAndBases() { AgcDriver::QueueState state; + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) execute(state, makePacket(0x69, {offset, 0xffffffffu})); + execute(state, makePacket(0x12, {0})); + for (const auto offset : {0x10bu, 0x10cu, 0x10du}) check(state.context.at(offset) == 0, "CLEAR_STATE did not restore stencil defaults"); execute(state, makePacket(0x69, {0x10, 17})); execute(state, makePacket(0x76, {0x20c, 2})); execute(state, makePacket(0x10, {3, 0}, 0x68)); @@ -352,6 +355,89 @@ void testIndirectDraw() { expectFailure([&] { AgcDriver::Pm4::ReadDrawCount(*local.indirect); }, "count address"); } +bool sameDraw(const AgcDriver::Pm4::DrawParameters& left, const AgcDriver::Pm4::DrawParameters& right) { + return left.indexAddress == right.indexAddress && left.indexCount == right.indexCount && left.indexSize == right.indexSize && left.instanceCount == right.instanceCount && + left.flags == right.flags && left.indexed == right.indexed && left.firstVertex == right.firstVertex && left.indirect.has_value() == right.indirect.has_value(); +} + +void testIndexAttributesIndirect() { + check(AgcDriver::Pm4::Name(0xc0029100u) == "INDEX_ATTRIBUTES_INDIRECT" && AgcDriver::Pm4::UnsupportedReason(0xc0029100u).empty(), "INDEX_ATTRIBUTES_INDIRECT is not executed"); + check(AgcDriver::Pm4::AccessesMemory(0xc0029100u), "INDEX_ATTRIBUTES_INDIRECT must synchronize guest memory"); + alignas(4) static std::array indices{}; + const auto indexAddress = reinterpret_cast(indices.data()) + 4; + alignas(16) static std::array records{}; + records.fill(0xdeadbeefu); + records[4] = static_cast(indexAddress); + records[5] = static_cast(indexAddress >> 32u); + records[6] = 12; + records[7] = 1; + const auto packetFor = [](const void* base, std::uint32_t index) { return makePacket(0x91, {low(base), high(base), index}); }; + const auto packet = packetFor(records.data(), 1); + AgcDriver::QueueState loaded; + loaded.indexBase = 0x1234; + loaded.indexBufferSize = 77; + loaded.indexType = 2; + execute(loaded, packet); + check(loaded.indexBase == indexAddress && loaded.indexBufferSize == 12 && loaded.indexType == 1, "INDEX_ATTRIBUTES_INDIRECT did not load INDEX_BASE, INDEX_BUFFER_SIZE and INDEX_TYPE from its record"); + AgcDriver::QueueState direct; + execute(direct, makePacket(0x26, {static_cast(indexAddress), static_cast(indexAddress >> 32u)})); + execute(direct, makePacket(0x13, {12})); + execute(direct, makePacket(0x2a, {1})); + const auto offsetDraw = makePacket(0x35, {12, 1, 5, 0}); + const auto fromRecord = AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); + check(sameDraw(fromRecord, AgcDriver::Pm4::ResolveDraw(offsetDraw, direct)) && fromRecord.indexAddress == indexAddress + 4 && fromRecord.indexSize == 4 && fromRecord.indexCount == 5, "an indexed draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + execute(loaded, makePacket(0x11, {1, low(records.data()), high(records.data())})); + execute(direct, makePacket(0x11, {1, low(records.data()), high(records.data())})); + const auto indirectDraw = makePacket(0x25, {0, 0x8c, 0x280, 0}); + const auto indirectFromRecord = AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); + check(sameDraw(indirectFromRecord, AgcDriver::Pm4::ResolveDraw(indirectDraw, direct)) && indirectFromRecord.indexAddress == indexAddress && indirectFromRecord.indexCount == 12 && indirectFromRecord.indexSize == 4, "an indexed indirect draw after INDEX_ATTRIBUTES_INDIRECT differs from the same draw after the direct packets"); + records[6] = 0x7fffffffu; + execute(loaded, packet); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(indirectDraw, loaded); }, "guest"); + records[6] = 12; + + static std::size_t flushed = 0; + flushed = 0; + struct FlushHookReset { + ~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); } + } reset; + AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) { + check(address == reinterpret_cast(records.data() + 4) && bytes == 16, "INDEX_ATTRIBUTES_INDIRECT synchronized an unrelated range"); + records[6] = 9; + ++flushed; + }); + execute(loaded, packet); + check(flushed == 1 && loaded.indexBufferSize == 9, "INDEX_ATTRIBUTES_INDIRECT read its record before the GPU work that writes it"); + AgcDriver::GuestMemory::SetFlushHook(nullptr); + records[6] = 12; + + const auto unchanged = [&](const std::vector& rejected, const char* text) { + AgcDriver::QueueState state; + state.indexBase = 0x40; + state.indexBufferSize = 3; + state.indexType = 2; + expectFailure([&] { execute(state, rejected); }, text); + check(state.indexBase == 0x40 && state.indexBufferSize == 3 && state.indexType == 2, "a rejected INDEX_ATTRIBUTES_INDIRECT changed the index state"); + }; + unchanged(makePacket(0x91, {low(records.data()) | 1u, high(records.data()), 1}), "indirect mode"); + unchanged(makePacket(0x91, {low(records.data()) | 4u, high(records.data()), 1}), "16-byte aligned"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data()), 0x10001u}), "reserved attribute index bits"); + unchanged(makePacket(0x91, {low(records.data()), high(records.data())}), "packet size"); + unchanged(packetFor(nullptr, 0), "null or misaligned"); + unchanged(packetFor(reinterpret_cast(0x1000), 0), "guest"); + unchanged(makePacket(0x91, {0xfffffff0u, 0xffffffffu, 1}), "record address overflow"); + expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0x20); }, "compute queue"); + records[7] = 4; + unchanged(packet, "has index type 0x4"); + char named[64]; + std::snprintf(named, sizeof(named), "record at 0x%llx", static_cast(reinterpret_cast(records.data() + 4))); + unchanged(packet, named); + records[7] = 3; + execute(loaded, packet); + expectFailure([&] { AgcDriver::Pm4::ResolveDraw(offsetDraw, loaded); }, "unsupported index type"); + records[7] = 1; +} + void testMemory() { AgcDriver::QueueState state; std::array data{0, 0, 0, 0}; @@ -836,6 +922,23 @@ void testRegisterListsReadAtSubmission() { check(std::atomic_ref(done).load() == 1, "register list rewritten after submission was read by the worker"); } +void testIndexAttributesReadAtExecution() { + alignas(8) static std::uint32_t gate = 0; + static std::uint32_t done = 0; + alignas(16) static std::array record{0, 0, 0, 4}; + gate = 0; + done = 0; + auto words = joinPackets({ + makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), + makePacket(0x91, {low(record.data()), high(record.data()), 0}), + writeWord(done, 1)}); + submitWords(words); + record = {0x1000, 0, 6, 0}; + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + check(std::atomic_ref(done).load() == 1, "INDEX_ATTRIBUTES_INDIRECT did not read its record when the packet ran"); +} + void testSuspendPointWritesQueuedLabels() { alignas(8) static std::uint32_t gate = 0, marker = 0; gate = 0; @@ -1136,6 +1239,7 @@ int main(int argc, char** argv) { testIndexedDraw(); testAutoDraw(); testIndirectDraw(); + testIndexAttributesIndirect(); testMemory(); testCopies(); testMemoryCopyDecode(); @@ -1150,6 +1254,7 @@ int main(int argc, char** argv) { testUnwrittenUserData(); testDriverSubmission(); testRegisterListsReadAtSubmission(); + testIndexAttributesReadAtExecution(); testPredicatedSubmission(); testConditionalSubmission(); testSuspendPointWritesQueuedLabels(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp new file mode 100644 index 0000000000..da2323b3de --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PresentationPass.cpp @@ -0,0 +1,538 @@ +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::DisplayBuffer; +using AgcDriver::PresentationPass; +using AgcDriver::PresentationScaler; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; +constexpr std::uint64_t TenBit = 0x0100000000000000ull; +constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0, std::uint32_t layers = 1) : context(context), extent(extent) { + 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, 1}; + info.mipLevels = 1; + info.arrayLayers = layers; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage resident present 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); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory resident present test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory resident present test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess, std::uint32_t layers = 1) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.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); +} + +std::vector bytesOf(Buffer& buffer, std::size_t size) { + const auto bytes = buffer.Bytes(); + return {bytes.begin(), bytes.begin() + static_cast(size)}; +} + +void letterboxBandTests() { + const std::array, 7> cases{{{3840, 2160, 2261, 1272}, {3840, 2160, 3840, 2160}, {3840, 2160, 1000, 1000}, {3840, 2160, 2000, 500}, {200, 130, 260, 130}, {200, 130, 117, 76}, {7, 3, 5, 11}}}; + for (const auto& [width, height, destinationWidth, destinationHeight] : cases) { + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(width, height, destinationWidth, destinationHeight); + const auto bands = PresentationScaler::LetterboxBands(width, height, destinationWidth, destinationHeight); + std::vector covered(static_cast(destinationWidth) * destinationHeight, 0); + auto cover = [&](const AgcDriver::AspectFitRect& area) { + Require(area.width != 0 && area.height != 0, "an empty letterbox band"); + for (std::uint32_t y = 0; y < area.height; ++y) { + for (std::uint32_t x = 0; x < area.width; ++x) ++covered[static_cast(area.y + y) * destinationWidth + area.x + x]; + } + }; + cover(rect); + for (const auto& band : bands) cover(band); + Require(std::all_of(covered.begin(), covered.end(), [](std::uint8_t count) { return count == 1; }), "the letterbox bands and the image do not tile the destination: " + std::to_string(width) + "x" + std::to_string(height) + " into " + std::to_string(destinationWidth) + "x" + std::to_string(destinationHeight)); + } + Require(PresentationScaler::LetterboxBands(3840, 2160, 1920, 1080).empty(), "an exact fit has letterbox bands"); +} + +struct Source { + std::uint64_t pixelFormat; + VkFormat storage; + std::uint32_t width; + std::uint32_t height; + std::vector words; + std::vector decoded; +}; + +Source makeSource(std::uint64_t pixelFormat, VkFormat storage, std::uint32_t width, std::uint32_t height, std::uint32_t salt = 0) { + Source source{pixelFormat, storage, width, height, std::vector(static_cast(width) * height * 4), {}}; + std::uint32_t seed = 0x9e3779b9u ^ static_cast(storage) ^ static_cast(pixelFormat >> 32u) ^ salt; + for (auto& byte : source.words) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + const ColorTargetLayout layout(width, height, ColorTileMode::RenderTarget); + std::vector storageBytes(layout.Bytes() + 65536); + auto* aligned = reinterpret_cast((reinterpret_cast(storageBytes.data()) + 65535u) & ~std::uintptr_t{65535}); + std::span guest(aligned, layout.Bytes()); + layout.Tile(source.words, guest); + const DisplayBuffer display{reinterpret_cast(aligned), pixelFormat, width, height}; + Require(AgcDriver::DisplayBufferSize(display) == guest.size(), "test display buffer size differs from the color layout"); + source.decoded = AgcDriver::DecodeDisplayBuffer(display, guest); + return source; +} + +std::unique_ptr residentImage(const Context& context, const std::vector& slices) { + const auto& source = *slices.front(); + const auto layers = static_cast(slices.size()); + auto image = std::make_unique(context, source.storage, VkExtent2D{source.width, source.height}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT, layers); + Buffer upload(context, source.words.size() * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::vector regions(layers); + for (std::uint32_t layer = 0; layer < layers; ++layer) { + std::memcpy(upload.Bytes().data() + source.words.size() * layer, slices[layer]->words.data(), source.words.size()); + regions[layer].bufferOffset = source.words.size() * layer; + regions[layer].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; + regions[layer].imageExtent = {source.width, source.height, 1}; + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT, layers); + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layers, regions.data()); + transition(context, commands, image->Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT, layers); + batch.SubmitAndWait(); + return image; +} + +std::unique_ptr residentImage(const Context& context, const Source& source) { return residentImage(context, std::vector{&source}); } + +std::vector convertPathPresent(const Context& context, const Source& source, VkExtent2D extent) { + Buffer upload(context, source.decoded.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(upload.Bytes().data(), source.decoded.data(), source.decoded.size()); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + scaler.EnsureSourceImage(source.width, source.height); + CommandBatch batch(context); + const auto commands = batch.Handle(); + scaler.RecordUpload(commands, upload.Handle()); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + scaler.RecordBlit(commands, destination.Handle(), extent.width, extent.height); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + return bytesOf(readback, bytes); +} + +std::vector passPresent(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, VkExtent2D extent, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(0, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), format, extent, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + return bytesOf(readback, bytes); +} + +std::vector passDump(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t stride, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + const auto bytes = static_cast(extent.width) * extent.height * 4; + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + pass.Bind(1, resident.Handle(), source.width, source.height, source.pixelFormat, whiteNits, layer); + { + CommandBatch batch(context); + pass.RecordDump(batch.Handle(), 1, stride, readback.Handle()); + batch.SubmitAndWait(); + } + pass.Release(1); + return bytesOf(readback, bytes); +} + +std::string describe(const Source& source, VkExtent2D extent) { + return std::string(AgcDriver::DisplayPq(source.pixelFormat) ? "PQ " : AgcDriver::DisplayTenBit(source.pixelFormat) ? "10-bit " : "8-bit ") + (AgcDriver::DisplayRedLow(source.pixelFormat) ? "red-low" : "blue-low") + " display in VkFormat " + std::to_string(static_cast(source.storage)) + " at " + std::to_string(extent.width) + "x" + std::to_string(extent.height); +} + +void passMatchesConvertPath(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const std::array exact{{{source.width, source.height}, {source.width + 60, source.height}, {source.width, source.height + 51}}}; + for (const auto extent : exact) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + Require(expected == drawn, "the presentation pass differs from the convert path: " + describe(source, extent)); + } + const std::array scaled{{{117, 76}, {331, 215}, {150, 150}}}; + for (const auto extent : scaled) { + const auto expected = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM); + int worst = 0; + std::size_t differing = 0; + for (std::size_t i = 0; i < expected.size(); ++i) { + const int difference = std::abs(static_cast(expected[i]) - static_cast(drawn[i])); + worst = std::max(worst, difference); + differing += difference != 0; + } + std::printf(" %s: scaled, %zu of %zu channels differ, by at most %d\n", describe(source, extent).c_str(), differing, expected.size(), worst); + Require(worst <= 2, "the scaled presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } +} + +void passDumpMatchesSubsample(const Context& context, PresentationPass& pass, const Source& source, const Image& resident, std::uint32_t layer = 0) { + for (const std::uint32_t stride : {1u, 3u, 4u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(passDump(context, pass, source, resident, stride, layer) == expected, "the presentation dump differs from the subsampled guest-path frame: " + describe(source, extent) + " stride " + std::to_string(stride)); + } +} + +bool renderable(const Context& context, VkFormat format) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; + return (properties.optimalTilingFeatures & needed) == needed; +} + +void passKeepsTenBits(const Context& context, PresentationPass& pass, const Source& source, const Image& resident) { + const bool redLowSource = AgcDriver::DisplayRedLow(source.pixelFormat); + for (const auto format : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + if (!renderable(context, format)) { + std::printf(" 10-bit destination VkFormat %d is not renderable here; skipped\n", static_cast(format)); + continue; + } + const bool redLowDestination = format == VK_FORMAT_A2B10G10R10_UNORM_PACK32; + const VkExtent2D extent{source.width + 40, source.height}; + const auto rect = AgcDriver::ComputeContainRect_nid_postfix(source.width, source.height, extent.width, extent.height); + const auto converted = convertPathPresent(context, source, extent); + const auto drawn = passPresent(context, pass, source, resident, extent, format); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const auto index = static_cast(y) * extent.width + x; + std::uint32_t word = 0; + std::memcpy(&word, drawn.data() + index * 4, 4); + const std::uint32_t low = word & 0x3ffu, green = (word >> 10u) & 0x3ffu, high = (word >> 20u) & 0x3ffu; + const std::uint32_t red = redLowDestination ? low : high, blue = redLowDestination ? high : low; + const bool inside = static_cast(x) >= rect.x && static_cast(x) < rect.x + static_cast(rect.width); + if (!inside) { + Require(word == 0xc0000000u, "a 10-bit letterbox band is not opaque black: " + describe(source, extent)); + continue; + } + std::uint32_t sourceWord = 0; + std::memcpy(&sourceWord, source.words.data() + (static_cast(y) * source.width + (x - static_cast(rect.x))) * 4, 4); + const std::uint32_t sourceLow = sourceWord & 0x3ffu, sourceGreen = (sourceWord >> 10u) & 0x3ffu, sourceHigh = (sourceWord >> 20u) & 0x3ffu; + const std::uint32_t sourceRed = redLowSource ? sourceLow : sourceHigh, sourceBlue = redLowSource ? sourceHigh : sourceLow; + Require(red == sourceRed && green == sourceGreen && blue == sourceBlue && (word >> 30u) == 3u, "the 10-bit presentation lost bits: " + describe(source, extent) + " into VkFormat " + std::to_string(static_cast(format))); + const auto* eight = converted.data() + index * 4; + Require(static_cast(eight[0]) == (blue >> 2u) && static_cast(eight[1]) == (green >> 2u) && static_cast(eight[2]) == (red >> 2u), "the 10-bit presentation's top bits differ from the convert path: " + describe(source, extent)); + } + } + } +} + +void blitLetterboxMatchesClear(const Context& context) { + const auto source = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto resident = residentImage(context, source); + PresentationScaler scaler(context, VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_UNORM); + for (const auto extent : {VkExtent2D{200, 130}, VkExtent2D{260, 130}, VkExtent2D{200, 181}, VkExtent2D{117, 76}, VkExtent2D{331, 300}}) { + const auto bytes = static_cast(extent.width) * extent.height * 4; + std::array, 2> results; + for (std::size_t banded = 0; banded < 2; ++banded) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + if (banded != 0) { + scaler.RecordLetterbox(commands, source.width, source.height, destination.Handle(), extent.width, extent.height); + } else { + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + } + PresentationScaler::RecordBlitFrom(context, commands, resident->Handle(), VK_IMAGE_LAYOUT_GENERAL, source.width, source.height, VK_FILTER_LINEAR, destination.Handle(), extent.width, extent.height); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + results[banded] = bytesOf(readback, bytes); + } + Require(results[0] == results[1], "the letterbox bands differ from a full clear: " + describe(source, extent)); + } +} + +void passPresentsSlice(const Context& context, PresentationPass& pass) { + const auto first = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130); + const auto second = makeSource(Bgra8, VK_FORMAT_B8G8R8A8_UNORM, 200, 130, 0x5a5a5a5au); + Require(first.decoded != second.decoded, "the two slices of the array test image hold the same texels"); + const auto resident = residentImage(context, std::vector{&first, &second}); + const std::array slices{&first, &second}; + for (std::uint32_t layer = 0; layer < slices.size(); ++layer) { + const auto& source = *slices[layer]; + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}}) { + Require(passPresent(context, pass, source, *resident, extent, VK_FORMAT_B8G8R8A8_UNORM, layer) == convertPathPresent(context, source, extent), "the presentation pass of slice " + std::to_string(layer) + " of an array image differs from the convert path: " + describe(source, extent)); + } + passDumpMatchesSubsample(context, pass, source, *resident, layer); + } +} + +void passRefusesHalfFloat(PresentationPass& pass, const Image& resident) { + bool refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgba16Float, AgcDriver::SdrToneMapLuminanceNits); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a 16-bit float display buffer"); + refused = false; + try { + pass.Bind(1, resident.Handle(), 200, 130, Rgb10Pq, 0.0f); + } catch (const std::exception&) { + refused = true; + } + Require(refused, "the presentation pass took a white level below 1 nit"); +} + +int largestDifference(const std::vector& expected, const std::vector& actual) { + Require(expected.size() == actual.size(), "compared frames differ in size"); + int worst = 0; + for (std::size_t i = 0; i < expected.size(); ++i) worst = std::max(worst, std::abs(std::to_integer(expected[i]) - std::to_integer(actual[i]))); + return worst; +} + +Source convertedAt(const Source& source, float whiteNits) { + auto converted = source; + for (std::size_t i = 0; i < converted.decoded.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, source.words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(source.pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) converted.decoded[i + channel] = static_cast(pixel[channel]); + } + return converted; +} + +void passConvertsPq(const Context& context, PresentationPass& pass, const Source& guest, const Image& resident, float whiteNits) { + const auto source = convertedAt(guest, whiteNits); + for (const auto extent : {VkExtent2D{source.width, source.height}, VkExtent2D{source.width + 60, source.height}, VkExtent2D{source.width, source.height + 51}}) { + Require(largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)) <= 1, "the PQ presentation pass differs from the CPU conversion by more than one level: " + describe(source, extent)); + } + for (const auto extent : {VkExtent2D{117, 76}, VkExtent2D{331, 215}}) { + const int worst = largestDifference(convertPathPresent(context, source, extent), passPresent(context, pass, source, resident, extent, VK_FORMAT_B8G8R8A8_UNORM, 0, whiteNits)); + std::printf(" %s, white %.0f nits: scaled, by at most %d\n", describe(source, extent).c_str(), whiteNits, worst); + Require(worst <= 2, "the scaled PQ presentation pass differs from the convert path's filtered blit by more than two levels: " + describe(source, extent)); + } + for (const std::uint32_t stride : {1u, 3u}) { + const auto extent = PresentationPass::DumpExtent(source.width, source.height, stride); + std::vector expected(static_cast(extent.width) * extent.height * 4); + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) std::memcpy(expected.data() + (static_cast(y) * extent.width + x) * 4, source.decoded.data() + (static_cast(y) * stride * source.width + x * stride) * 4, 4); + } + Require(largestDifference(expected, passDump(context, pass, source, resident, stride, 0, whiteNits)) <= 1, "the PQ presentation dump differs from the subsampled CPU conversion: " + describe(source, extent) + " stride " + std::to_string(stride)); + } +} + +void residentPresentTests(const Context& context) { + PresentationPass pass(context, 2); + const std::array, 5> cases{{ + {Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, + {Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, + {Bgra8, VK_FORMAT_R8G8B8A8_UNORM}, + {Rgba8, VK_FORMAT_B8G8R8A8_UNORM}, + }}; + for (const auto& [pixelFormat, storage] : cases) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passMatchesConvertPath(context, pass, source, *resident); + passDumpMatchesSubsample(context, pass, source, *resident); + if (AgcDriver::DisplayTenBit(pixelFormat)) passKeepsTenBits(context, pass, source, *resident); + if (pixelFormat == Bgra8) passRefusesHalfFloat(pass, *resident); + } + for (const auto& [pixelFormat, storage] : {std::pair{Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32}, std::pair{Bgr10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32}}) { + const auto source = makeSource(pixelFormat, storage, 200, 130); + const auto resident = residentImage(context, source); + passConvertsPq(context, pass, source, *resident, AgcDriver::SdrToneMapLuminanceNits); + passConvertsPq(context, pass, source, *resident, 1000.0f); + } + passPresentsSlice(context, pass); + blitLetterboxMatchesClear(context); +} + +} + +int main() { + try { + letterboxBandTests(); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + residentPresentTests(device->GetContext()); + std::cout << "resident present format, presentation pass and letterbox 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/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 815cf01fcb..ce1b896d3e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -53,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -577,6 +579,158 @@ void readTrackingTests(const Device& device, Recorder& recorder) { Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x20000, 0x1000, false), "noted ranges outlived their batch"); } +Recorder::WriteRanges expectedWriteUnion(Recorder::WriteRanges ranges) { + std::sort(ranges.begin(), ranges.end()); + Recorder::WriteRanges result; + for (const auto& [begin, end] : ranges) { + if (!result.empty() && begin <= result.back().second) result.back().second = std::max(result.back().second, end); + else result.emplace_back(begin, end); + } + return result; +} + +void pendingWriteSnapshotTests(const Device& device, Recorder& recorder) { + recorder.Sync(); + Recorder::WriteRanges expected; + const auto check = [&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && *snapshot == expectedWriteUnion(expected), "pending write snapshot differs from the full union"); + }; + const auto note = [&](std::uint64_t address, std::size_t bytes) { + const auto previous = Recorder::PendingWriteSnapshot(); + const auto saved = previous == nullptr ? Recorder::WriteRanges{} : *previous; + recorder.NotePendingWrite(address, bytes); + if (bytes != 0) expected.emplace_back(address, address + bytes); + check(); + Require(previous == nullptr || *previous == saved, "publishing a new write modified an older reader's snapshot"); + }; + note(0x10000, 16); + note(0x10020, 16); + note(0x10010, 16); + note(0x10004, 4); + note(0x10000, 0); + note(0, 1); + note(UINT64_MAX - 16, 16); + const Recorder::WriteRanges additions{{0x20020, 0x20040}, {0x20000, 0x20020}, {0x20010, 0x20030}, {0x20040, 0x20040}, {0x20050, 0x20040}}; + recorder.NotePendingWrites(additions); + for (const auto& range : additions) if (range.second > range.first) expected.push_back(range); + check(); + recorder.NotePendingFill(0x30000, 16, 0xA5); + expected.emplace_back(0x30000, 0x30010); + check(); + alignas(64) static std::array labels{}; + const auto labelAddress = reinterpret_cast(labels.data()); + const std::array value{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + recorder.NoteLabel(labelAddress, value, 1, 0); + expected.emplace_back(labelAddress, labelAddress + value.size()); + check(); + recorder.Submit(); + device.WaitQueue(); + recorder.AfterCompletions(labelAddress + 8, value, 2, 0, true); + expected.emplace_back(labelAddress + 8, labelAddress + 8 + value.size()); + check(); + note(0x40000, 16); + bool completing = false; + recorder.OnComplete([&] { + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(Recorder::SnapshotOverlaps(snapshot.get(), 0x40000, 16), "a finishing batch left the snapshot before its completion"); + completing = true; + }); + recorder.Sync(); + Require(completing && Recorder::PendingWriteSnapshot()->empty(), "completed writes remained published"); + std::mt19937_64 random(0x41505335u); + for (int trial = 0; trial < 16; ++trial) { + expected.clear(); + for (int step = 0; step < 64; ++step) note(0x80000 + random() % 4096, 1 + random() % 128); + recorder.Sync(); + Require(Recorder::PendingWriteSnapshot()->empty(), "random writes survived their GPU batch"); + } + expected.clear(); + note(0x90000, 16); + { + std::atomic invalid{false}; + std::atomic ready{0}; + std::atomic reads{0}; + std::array readers; + for (auto& reader : readers) reader = std::jthread([&](std::stop_token stop) { + ready.fetch_add(1, std::memory_order_release); + while (!stop.stop_requested()) { + const auto snapshot = Recorder::PendingWriteSnapshot(); + if (snapshot == nullptr || !Recorder::SnapshotOverlaps(snapshot.get(), 0x90000, 16)) invalid.store(true, std::memory_order_relaxed); + if (snapshot != nullptr) { + for (std::size_t i = 0; i < snapshot->size(); ++i) { + const auto [begin, end] = (*snapshot)[i]; + if (end <= begin || (i != 0 && (*snapshot)[i - 1].second >= begin)) invalid.store(true, std::memory_order_relaxed); + } + } + reads.fetch_add(1, std::memory_order_relaxed); + } + }); + while (ready.load(std::memory_order_acquire) != readers.size()) std::this_thread::yield(); + for (std::uint64_t i = 0; i < 256; ++i) note(0x91000 + i * 128, 32); + for (auto& reader : readers) reader.request_stop(); + for (auto& reader : readers) reader.join(); + Require(reads.load() != 0 && !invalid.load(), "concurrent snapshot readers observed an invalid or incomplete union"); + } + recorder.Sync(); + expected.clear(); + note(UINT64_MAX - 3, 8); + note(0x50000, 16); + recorder.Sync(); + recorder.NotePendingWrite(0x58000, 16); + { + Recorder other(device.GetContext()); + other.Activate(); + recorder.NotePendingWrite(0x59000, 16); + recorder.Activate(); + recorder.NotePendingWrite(0x5A000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x58000, 0x58010}, {0x59000, 0x59010}, {0x5A000, 0x5A010}}, "reactivation missed writes recorded while inactive"); + other.Activate(); + recorder.Sync(); + recorder.Activate(); + recorder.NotePendingWrite(0x5B000, 16); + Require(*Recorder::PendingWriteSnapshot() == Recorder::WriteRanges{{0x5B000, 0x5B010}}, "reactivation retained writes completed while inactive"); + recorder.Sync(); + other.Activate(); + other.NotePendingWrite(0x60000, 16); + const auto otherSnapshot = Recorder::PendingWriteSnapshot(); + recorder.Activate(); + recorder.NotePendingWrite(0x70000, 16); + const auto ownSnapshot = Recorder::PendingWriteSnapshot(); + Require(ownSnapshot != nullptr && *ownSnapshot == Recorder::WriteRanges{{0x70000, 0x70010}}, "a recorder extended another recorder's snapshot"); + Require(*otherSnapshot == Recorder::WriteRanges{{0x60000, 0x60010}}, "a replaced owner's retained snapshot changed"); + other.Sync(); + Require(Recorder::PendingWriteSnapshot() == ownSnapshot, "an inactive recorder replaced the active snapshot"); + recorder.Sync(); + } + Require(Recorder::PendingWriteSnapshot()->empty(), "snapshot ownership tests left pending writes"); + std::cout << "Pending write snapshot differential, lifetime, completion and ownership tests passed\n"; +} + +void pendingWriteSnapshotBenchmark(Recorder& recorder) { + for (std::uint64_t distinct : {8u, 32u, 512u}) { + for (int duplicates : {0, 8, 64}) { + std::array samples{}; + for (auto& sample : samples) { + recorder.Sync(); + static_cast(recorder.Commands()); + const auto start = std::chrono::steady_clock::now(); + for (std::uint64_t i = 0; i < distinct; ++i) { + const auto address = 0x100000 + i * 4096; + recorder.NotePendingWrite(address, 256); + for (int repeat = 0; repeat < duplicates; ++repeat) recorder.NotePendingWrite(address, 256); + } + sample = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + const auto snapshot = Recorder::PendingWriteSnapshot(); + Require(snapshot != nullptr && snapshot->size() == distinct, "snapshot benchmark produced another union"); + } + std::sort(samples.begin(), samples.end()); + std::cout << "snapshot-note benchmark: " << distinct << " distinct, " << duplicates << " repeats, median " << samples[2] << " ms\n"; + } + } + recorder.Sync(); +} + void writeSettledTests(const Device& device, Recorder& recorder) { Require(recorder.PendingWriteSettled(0x50000, 0x100), "an unwritten range is not settled"); recorder.NotePendingWrite(0x50000, 0x100); @@ -1310,6 +1464,41 @@ void hostImportRetirementTests(const Device& device) { Require(preservedEveryRecorder, "unlocked mapping changed recorder batches"); std::cout << "Host import retirement locking and GPU readback tests passed\n"; } +#ifdef _WIN32 +void secondMappingImportTests(const Device& device) { + const auto& context = device.GetContext(); + if (context.hostImportAlignment == 0) { + std::cout << "host imports unavailable: imports after a second mapping not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + constexpr std::size_t page = 16384; + struct Release { + void* mapping = nullptr; + void* alias = nullptr; + std::int64_t physical = -1; + ~Release() { + if (alias != nullptr) sceKernelMunmap(alias, page); + if (mapping != nullptr) sceKernelMunmap(mapping, bytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, bytes); + } + } release; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, bytes, 0, &release.physical) == 0, "allocate the second mapping test backing"); + Require(sceKernelMapDirectMemory(&release.mapping, bytes, 0x33, 0, release.physical, bytes) == 0, "map the second mapping test backing"); + const auto address = reinterpret_cast(release.mapping); + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of singly mapped direct memory refused: imports after a second mapping not tested\n"; + return; + } + const auto first = HostImportSerial(context, address, bytes, true); + Require(first != 0 && HostImportSerial(context, address, bytes, true) == first, "singly mapped direct memory lost its import"); + Require(sceKernelMapDirectMemory(&release.alias, page, 0x33, 0, release.physical, page) == 0, "map the first page a second time"); + Require(HostImportFor(context, address, bytes) != nullptr, "direct memory was not imported again after a second mapping of its first page"); + const auto second = HostImportSerial(context, address, bytes, true); + Require(second != 0 && second != first, "the import of direct memory whose pages were replaced was kept"); + HostImportFor(context, address, bytes); +} +#endif void movedMetadataTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); @@ -2234,6 +2423,19 @@ void drawSnapshotCapTests(const Device& device, Recorder& recorder) { Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write"); std::memset(block, 0x11, bytes); snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "64") == 0, "cannot raise snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "64", 1) == 0, "cannot raise snapshot cap"); +#endif + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == 1 && again->snapshots[0].address == smallAddress, "built resources read the draw snapshot cap again instead of keeping the one they were built with"); + snapshotRecorder.Sync(); +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot restore snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot restore snapshot cap"); +#endif } const auto firstAddress = address + 4096; constexpr std::size_t firstBytes = 600 * 1024; @@ -4255,11 +4457,13 @@ void queuedWriteGroupTests() { }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); 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{6}, "a clear, a begin dump and an end dump per query slot are not one group"); + 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}}; Require(Recorder::QueuedWriteGroups(reused) == Groups{1, 3}, "a store over a computed write of its group did not start a new group"); const std::vector apart{{0x100, 0x1f8, true}, {0x1f8, 0x200, false}, {0x300, 0x310, false}}; - Require(Recorder::QueuedWriteGroups(apart) == Groups{3}, "stores beside the computed writes left their group"); + Require(Recorder::QueuedWriteGroups(apart) == Groups{1, 3}, "stores after the computed writes joined their group"); + const std::vector label{{0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x400, 0x408, false}}; + Require(Recorder::QueuedWriteGroups(label) == Groups{2, 3}, "a label after the dumps landed before them"); const std::vector stores{{0x100, 0x110, false}, {0x100, 0x110, false}}; Require(Recorder::QueuedWriteGroups(stores) == Groups{2}, "stores alone were split into groups"); std::uint64_t seed = 0x9e3779b97f4a7c15ull; @@ -4562,7 +4766,31 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill did not read as its keys without a wait"); recorder.NotePendingWrite(address, 2 * keyCount); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later wider writer over a pending fill was not waited for"); + recorder.NotePendingFill(address, keyCount / 2, 0x40); + recorder.NotePendingFill(address + keyCount / 2, keyCount / 2, 0x40); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of pending 0001 fills over both halves did not read as 0001 without a wait"); + recorder.NotePendingWrite(address + keyCount / 2, 16); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed && !recorder.PendingWriteOverlaps(address, keyCount), "a later writer of unknown bytes over pending fills was not waited for"); + recorder.NotePendingFill(address, keyCount / 2, 0x00); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Mixed && recorder.PendingWriteOverlaps(address, keyCount), "a pending 0000 fill over half of uncompressed keys did not read as mixed without a wait"); recorder.Sync(); +#ifdef _WIN32 + auto* edgeBlock = static_cast(VirtualAlloc(nullptr, 2 * bytes, MEM_RESERVE, PAGE_NOACCESS)); + Require(edgeBlock != nullptr && VirtualAlloc(edgeBlock, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot reserve the key block past committed memory"); +#else + void* edgeMapping = mmap(nullptr, 2 * bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(edgeMapping != MAP_FAILED && mprotect(edgeMapping, bytes, PROT_READ | PROT_WRITE) == 0, "cannot reserve the key block past committed memory"); + auto* edgeBlock = static_cast(edgeMapping); +#endif + const auto edge = reinterpret_cast(edgeBlock) + bytes - keyCount / 2; + Require(!AgcDriver::GuestMemory::Accessible(reinterpret_cast(edge), keyCount), "keys past the end of committed memory read as accessible"); + recorder.NotePendingFill(edge, keyCount / 2, 0x40); + Require(CurrentDccKeys(edge, surfaceBytes) == DccKeys::Unreadable && !recorder.PendingWriteOverlaps(edge, keyCount), "keys running past the end of committed memory, with a pending fill over their committed half, were not read as unreadable after a wait"); +#ifdef _WIN32 + VirtualFree(edgeBlock, 0, MEM_RELEASE); +#else + munmap(edgeBlock, 2 * bytes); +#endif constexpr std::uint64_t displayBytes = 15u * 9u * 65536u; constexpr std::size_t consoleKeys = 49152; const auto displayKeys = DccKeyCount(TextureTileMode::kR64KBX, 4, 1920, 1080, displayBytes); @@ -4577,6 +4805,101 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +bool shadowedKeyStoreTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + if (!UnitShadowEnabled() || context.hostImportAlignment == 0) { + std::cout << "unit shadows or host imports unavailable: keys under a unit shadow not tested\n"; + return false; + } + constexpr std::uint64_t unit = 65536; + constexpr std::size_t bytes = 2 * unit; + constexpr std::size_t surfaceBytes = 65536; + constexpr std::size_t keyCount = surfaceBytes / 256; +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, unit); + Require(block != nullptr, "cannot reserve the shadowed key block"); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(bytes), nullptr); + Require(section != nullptr, "cannot create the shadowed key block"); + GuestArena::GuestArenaMap_nid_postfix(block, bytes, section, 0, PAGE_READWRITE); + CloseHandle(section); +#else + void* block = AllocateWatched(bytes, unit); + Require(block != nullptr, "cannot allocate the shadowed key block"); +#endif + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + auto* keys = static_cast(block); + std::memset(block, 0xff, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + } + } unregister{context, block, address}; + const auto* import = HostImportFor(context, address, bytes); + if (import == nullptr || !Watched(address, bytes)) { + std::cout << "the shadowed key block is not imported and watched: keys under a unit shadow not tested\n"; + return false; + } + recorder.Sync(); + CollectWritesUncached(address, bytes); + const auto copyBuffer = context.Function("vkCmdCopyBuffer"); + const auto shadow = [&](std::uint8_t value) { + auto destination = ShadowDestinationFor(context, *import, address, address + unit); + Require(destination.has_value() && destination->seedUnits.empty(), "the key unit has no unit shadow destination"); + auto pattern = std::make_shared(context, static_cast(unit), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memset(pattern->Bytes().data(), value, pattern->Bytes().size()); + const auto commands = recorder.Commands(); + recorder.Keep(pattern); + recorder.Keep(destination->slab); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const VkBufferCopy piece{0, destination->offset, unit}; + copyBuffer(commands, pattern->Handle(), destination->buffer, 1, &piece); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder.MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{address, address + unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), TrackerGeneration()); + Require(AnyShadowedOverlaps(address, keyCount), "the key unit is not shadowed"); + }; + recorder.NotePendingFill(address, unit, 0x00); + MarkWritten(address, unit); + shadow(0x40); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear0001 && !recorder.PendingWriteOverlaps(address, keyCount), "the keys under a fresh unit shadow were not read from its published bytes after a wait"); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40 && keys[unit] == 0xff && !AnyShadowedOverlaps(address, unit), "the fresh unit's publish did not land"); + recorder.Sync(); + shadow(0x22); + recorder.NotePendingFill(address, unit, 0xc0); + MarkWritten(address, unit); + Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Clear1111 && recorder.PendingWriteOverlaps(address, keyCount), "the keys of a pending fill over a stale unit shadow were not read as the fill's keys without a wait"); + Require(!AnyShadowedOverlaps(address, unit), "the stale unit still counts as shadowed after the key read"); + recorder.Sync(); + GuestTextureResource resource{}; + resource.baseAddress = address + unit; + resource.format = 56; + resource.dccAddress = address; + shadow(0x22); + recorder.NotePendingFill(address, unit, 0xc0); + MarkWritten(address, unit); + Require(!WaitForKeyWriters(resource, surfaceBytes).has_value() && recorder.PendingWriteOverlaps(address, keyCount), "the key writers over a stale unit shadow were waited for"); + recorder.Sync(); + Require(keys[0] == 0x40 && keys[unit - 1] == 0x40, "a stale unit's slab bytes were published"); + return true; +} + void sampleDumpTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0 || !context.bufferDeviceAddress) { @@ -4815,7 +5138,9 @@ void metadataPassTests(const Device& device, Recorder& recorder) { Require(keys[0] == 0xff, "the recorded key fill landed before its batch ran"); Require(CurrentDccKeys(color.dccAddress, surfaceBytes) == DccKeys::ClearRegister, "the keys read past a pending fast-clear fill"); } + std::memset(texels, 0x77, surfaceBytes); RunColorMetadataPass(context, pass); + Require(StorageTexture::FindPending(address, surfaceBytes) != nullptr && texels[0] == 0x77, "a fast clear recorded before the pass fell back to CPU write-back"); Require(keysUncompressed() && memoryHolds({0x10, 0x20, 0x40, 0x80}), "a fast clear recorded before the pass was not eliminated"); std::memset(texels, 0x66, surfaceBytes); RunColorMetadataPass(context, pass); @@ -5055,6 +5380,12 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { context.nullDescriptors = true; TextureCache cache(context); context.textureCache = &cache; + const auto clearTargets = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + ClearMultisampleTargets(context.device); + }; const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { std::string message; try { @@ -5111,7 +5442,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { }, "a texture read", "a texture read"); refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); - ClearMultisampleTargets(context.device); + clearTargets(); target.tileMode = ColorTileMode::RenderTarget; { std::lock_guard gpu(GpuMutex()); @@ -5122,7 +5453,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the tiled multisampled target"); AgcDriver::GuestMemory::Read(address + targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) { target.extent = {128, 64}; target.surfaceExtent = target.extent; @@ -5136,7 +5467,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 2x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) { target.extent = {64, 32}; @@ -5151,9 +5482,48 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } refused([&] { AgcDriver::GuestMemory::Read(address + 2u * targetBytes - texels.size(), texels); }, "a CPU read", "a CPU read inside the 8x tile-height padding"); AgcDriver::GuestMemory::Read(address + 2u * targetBytes, texels); - ClearMultisampleTargets(context.device); + clearTargets(); } AgcDriver::GuestMemory::Read(address + 256u, texels); + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.samples = 4; + VkImageView previous = VK_NULL_HANDLE; + for (const bool removeAllocation : {false, true}) { + VkImageView view = VK_NULL_HANDLE; + { + std::lock_guard gpu(GpuMutex()); + view = MultisampleTargetView(context, target); + Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + { + GuestAllocations::Mutation mutation; + mutation.Protect(block, bytes, true, true, true, [] {}); + } + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after protection changed"); + bool failed = false; + try { + GuestAllocations::Mutation mutation; + mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) { throw std::runtime_error("unmap failed"); }); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed, "the failed host unmap did not throw"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a CPU read"); }, "a CPU read", "a read after a failed unmap"); + { + GuestAllocations::Mutation mutation; + if (removeAllocation) mutation.Remove(block); + else mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {}); + mutation.Add(block, bytes, true, true, true); + } + RequireNoMultisampleTarget(address, bytes, "a texture read"); + AgcDriver::GuestMemory::Read(address + 256u, texels); + previous = view; + } + clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } @@ -5484,6 +5854,127 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { expectRed(program.Red(flatTexture.View(), flatTexture.Layout(), 0.0f), 0x20 / 255.0f, "a 2D texture over the surface does not read its first layer"); } +void sliceDirtyTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + constexpr std::uint32_t side = 128; + constexpr std::size_t sliceBytes = 65536; + constexpr std::size_t bytes = 4 * sliceBytes; + constexpr std::uint8_t untouched = 0x11; + for (const auto tileMode : {TextureTileMode::kLinear, TextureTileMode::kR64KBX}) { + const std::string what = tileMode == TextureTileMode::kLinear ? "a linear two-slice image" : "a tiled two-slice image"; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: slice dirty marking not tested\n"; + return; + } + auto* memory = static_cast(block); + std::memset(memory, untouched, bytes); + 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; + std::size_t bytes; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + ReleaseWatched(block, bytes); + } + } unregister{base, block, address, bytes}; + AgcDriver::GuestMemory::CollectWritesUncached(address, bytes); + TextureDetiler detiler(base); + auto context = base; + context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.depthOrLastArray = 1; + resource.mipCount = 1; + resource.tileMode = tileMode; + resource.dimension = TextureDimension::k2DArray; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.imageLayers == 2 && geometry.layerBytes == sliceBytes && geometry.guestBytes == 2 * sliceBytes, what + " does not hold two 64 KiB slices"); + const auto stored = [&](std::uint32_t slice, std::array color) { + std::vector read(sliceBytes); + AgcDriver::GuestMemory::Read(address + geometry.GuestLayerOffset(slice), read); + for (std::size_t at = 0; at < read.size(); ++at) { + if (std::to_integer(read[at]) != color[at % 4]) return false; + } + return true; + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + 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); + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + const VkClearColorValue green{{0.0f, 1.0f, 0.0f, 1.0f}}; + const VkImageSubresourceRange first{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkImageSubresourceRange second{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 1, 1}; + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &first); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &green, 1, &second); + 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->MarkSliceDirty(0); + for (std::uint32_t slice = 0; slice < 2; ++slice) { + const auto display = address + geometry.GuestLayerOffset(slice); + const auto found = StorageTexture::FindPendingSlice(display, sliceBytes); + Require(found == image, what + ": a display buffer that is slice " + std::to_string(slice) + " does not find the pending image"); + Require(AgcDriver::ResidentPresentSlice(found->Descriptor().baseAddress, found->GuestBytes(), found->ImageLayers(), display, sliceBytes) == slice, what + ": a display buffer that is slice " + std::to_string(slice) + " is not presented from that layer"); + } + Require(StorageTexture::FindPending(address + geometry.GuestLayerOffset(1), sliceBytes) == nullptr, what + ": the lookup by base address finds a display buffer in slice 1"); + Require(StorageTexture::FindPendingSlice(address + 2 * sliceBytes, sliceBytes) == nullptr && StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes + 1) == nullptr, what + ": a range past the image's last slice finds the pending image"); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}), what + ": the slice a draw wrote was not stored"); + Require(stored(1, {untouched, untouched, untouched, untouched}), what + ": a slice no draw wrote was stored over guest memory"); + static_cast(recorder.Commands()); + recorder.Keep(image); + image->MarkSliceDirty(1); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + Require(stored(0, {255, 0, 0, 255}) && stored(1, {0, 255, 0, 255}), what + ": the second slice was not stored once a draw wrote it"); + auto other = StorageTexture::Create(context, detiler, resource, 0); + static_cast(recorder.Commands()); + recorder.Keep(image); + recorder.Keep(other); + image->MarkSliceDirty(1); + other->MarkSliceDirty(1); + std::string ambiguous; + try { + static_cast(StorageTexture::FindPendingSlice(address + geometry.GuestLayerOffset(1), sliceBytes)); + } catch (const std::runtime_error& error) { + ambiguous = error.what(); + } + Require(ambiguous.find("is a slice of the pending array images") != std::string::npos, what + ": a slice of two pending array images did not throw: " + ambiguous); + image->WriteBack(); + other->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + recorder.Sync(); + } + std::cout << "slice dirty marking: ok\n"; +} + void atomicViewTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); TextureDetiler detiler(context); @@ -6113,8 +6604,155 @@ void pipelineLibraryTests(const Device& device) { std::cout << "Pipeline library reuse tests passed\n"; } +#ifdef _WIN32 +namespace StorageCpuSnapshotTest { +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::size_t BlockBytes = 12 * SurfaceBytes; + +class Block { +public: + explicit Block(const Context& context, bool watched = false) : context(context), watched(watched) { + if (watched) { + memory = GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot allocate the shared texture section"); + try { GuestArena::GuestArenaMap_nid_postfix(memory, BlockBytes, section, 0, PAGE_READWRITE); } + catch (...) { CloseHandle(section); throw; } + CloseHandle(section); + } else memory = VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Require(memory != nullptr, "cannot allocate the texture block"); + std::memset(memory, 0x5a, BlockBytes); + GuestAllocations::Mutation().Add(memory, BlockBytes, true, true, true); + } + + ~Block() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); + HostImportFor(context, Surface(0), BlockBytes); + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(memory, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, BlockBytes); + } else VirtualFree(memory, 0, MEM_RELEASE); + } + + std::uint64_t Surface(std::size_t surface) const { return reinterpret_cast(memory) + surface * SurfaceBytes; } + +private: + const Context& context; + bool watched; + void* memory = nullptr; +}; + +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), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) { + const auto words = Descriptor(address, side); + return DecodeTextureResource(words); +} + +void Run(Context context) { + TextureDetiler detiler(context); + context.detiler = &detiler; + for (const bool watched : {true, false}) { + Block block(context, watched); + const auto address = block.Surface(0); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "snapshot fixture has the wrong write tracking"); + Require(HostImportFor(context, address, SurfaceBytes) != nullptr, "snapshot fixture requires a host import"); + Require(AgcDriver::GuestMemory::Watched(address, SurfaceBytes) == watched, "host import changed the fixture's write tracking"); + auto image = StorageTexture::Create(context, *context.detiler, Resource(address), 0); + const auto expectedBytes = watched ? 0u : SurfaceBytes; + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched GPU-direct upload retained an unnecessary CPU surface snapshot"); + const auto readPixel = [&] { + Buffer result(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, result.Handle(), 1, ©); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + result.Invalidate(); + std::uint32_t pixel = 0; + std::memcpy(&pixel, result.Bytes().data(), sizeof(pixel)); + return pixel; + }; + Require(readPixel() == 0x5a5a5a5au, "snapshot upload returned incorrect GPU texels"); + std::memset(reinterpret_cast(address), 0x6b, SurfaceBytes); + image->Refresh(); + Require(readPixel() == 0x6b6b6b6bu, "snapshot refresh missed a CPU write"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "watched refresh allocated a CPU surface snapshot"); + { + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + VkClearColorValue clear{}; + clear.uint32[0] = 0x7c7c7c7cu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + std::uint32_t pixel = 0; + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x7c7c7c7cu, "snapshot write-back lost GPU texels"); + Require(image->OriginalSnapshotBytes() == expectedBytes, "GPU-direct write-back allocated a CPU surface snapshot"); + if (watched) { + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "snapshot fixture is still write-watched"); + std::memset(reinterpret_cast(address), 0x8d, SurfaceBytes); + image->Refresh(); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "unwatched refresh did not allocate its comparison snapshot"); + Require(readPixel() == 0x8d8d8d8du, "unwatched refresh returned incorrect GPU texels"); + } + image.reset(); + Context cpu = context; + cpu.hostImportAlignment = 0; + image = StorageTexture::Create(cpu, *context.detiler, Resource(address), 0); + Require(image->OriginalSnapshotBytes() == SurfaceBytes, "CPU upload has no guest-byte snapshot"); + const auto expectedPixel = watched ? 0x8d8d8d8du : 0x7c7c7c7cu; + Require(readPixel() == expectedPixel, "CPU upload returned incorrect GPU texels"); + { + CommandBatch batch(context); + VkClearColorValue clear{}; + clear.uint32[0] = 0x9e9e9e9eu; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(batch.Handle(), image->Image(), VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + batch.SubmitAndWait(); + } + image->MarkDirty(); + image->WriteBack(); + AgcDriver::GuestMemory::ReadCommitted(address, std::as_writable_bytes(std::span(&pixel, 1))); + Require(pixel == 0x9e9e9e9eu, "CPU write-back lost guest texels"); + std::printf("%s snapshot fixture: direct %zu bytes, CPU fallback %zu bytes\n", watched ? "watched" : "unwatched", expectedBytes, image->OriginalSnapshotBytes()); + } +} +} +#endif + int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--queued-write-groups-only") { + queuedWriteGroupTests(); + std::cout << "queued write group tests passed\n"; + return 0; + } std::unique_ptr created; try { created = std::make_unique(); @@ -6147,6 +6785,18 @@ int main(int argc, char** argv) { return 0; } std::unique_lock gpu(GpuMutex()); +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--storage-cpu-snapshot-only") { + if (device.GetContext().hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "storage snapshot test requires host imports"); + std::cout << "skipped, host imports unavailable\n"; + return 77; + } + StorageCpuSnapshotTest::Run(device.GetContext()); + std::cout << "Storage CPU snapshot tests passed\n"; + return 0; + } +#endif std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); @@ -6154,6 +6804,14 @@ int main(int argc, char** argv) { copiedDirectOrderingTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pending-write-snapshot-only") { + pendingWriteSnapshotTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--benchmark-pending-write-snapshot") { + pendingWriteSnapshotBenchmark(recorder); + return 0; + } if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) { storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only"); return 0; @@ -6195,6 +6853,14 @@ int main(int argc, char** argv) { hostImportUnmapTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--slice-dirty-only") { + sliceDirtyTests(device, recorder); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--resident-present-only") { + RunResidentPresentTests(device.GetContext()); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--parked-import-unmap-only") { gpu.unlock(); parkedImportUnmapTests(device); @@ -6213,6 +6879,28 @@ 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]) == "--metadata-pass-only") { + metadataPassTests(device, recorder); + std::cout << "Color metadata pass tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--pending-key-stores-only") { + if (device.GetContext().hostImportAlignment == 0) { + std::cout << "skipped, host imports are unavailable\n"; + return 77; + } + pendingKeyStoreTests(device, recorder); + std::cout << "Pending DCC key store tests passed\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { + if (!shadowedKeyStoreTests(device, recorder)) { + std::cout << "skipped, the device imports no watched host memory under a unit shadow\n"; + return 77; + } + std::cout << "DCC keys under unit shadows tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-inputs-in-place-only") { drawInputInPlaceTests(device, recorder); std::cout << "In-place draw input tests passed\n"; @@ -6237,6 +6925,12 @@ int main(int argc, char** argv) { return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; } +#ifdef _WIN32 + if (argc == 2 && std::string_view(argv[1]) == "--host-import-second-mapping-only") { + secondMappingImportTests(device); + return 0; + } +#endif readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); @@ -6264,9 +6958,13 @@ int main(int argc, char** argv) { drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); + sliceDirtyTests(device, recorder); storeRunTests(device, recorder); queuedWriteGroupTests(); remappedImportTests(device); +#ifdef _WIN32 + secondMappingImportTests(device); +#endif movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); @@ -6295,6 +6993,7 @@ int main(int argc, char** argv) { metadataPassTests(device, recorder); cmaskPassTests(device, recorder); pendingKeyStoreTests(device, recorder); + shadowedKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); copiedDirectOrderingTests(device, recorder); gpu.unlock(); diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp new file mode 100644 index 0000000000..55fb0a5297 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -0,0 +1,447 @@ +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/RedundantValueEliminator.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Builder { + IrProgram program; + IrBlock* block = nullptr; + + Builder() { + block = &newBlock(); + program.SetEntryBlock(*block); + } + + IrBlock& newBlock() { + auto& created = program.CreateBlock(); + program.BlockOrder().push_back(&created); + return created; + } + + IrValue& constant(std::uint32_t value) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::U32); + created.SetImmediateU32(value); + return created; + } + + IrValue& floatConstant(std::uint32_t bits) { + auto& created = program.CreateValue(IrOpcode::Void, IrType::F32); + created.SetImmediateU32(bits); + return created; + } + + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0u) { + auto& created = program.CreateValue(opcode, type, flags); + for (auto* argument : arguments) created.AddArgument(argument); + block->AppendInstruction(&created); + return created; + } + + IrValue& unknown() { + return emit(IrOpcode::LaneId, IrType::U32, {}); + } + + IrValue& flush(IrValue& bits) { + auto& exponent = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x7f800000u)}); + auto& denormal = emit(IrOpcode::IEqual32, IrType::Bool, {&exponent, &constant(0u)}); + auto& sign = emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &constant(0x80000000u)}); + return emit(IrOpcode::SelectU32, IrType::U32, {&denormal, &sign, &bits}); + } + + void keep(IrValue& value) { + static_cast(emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + } + + std::uint32_t eliminate() { + return RedundantValueEliminator{}.Eliminate(program).replacedValues; + } +}; + +bool expect(const char* name, bool condition) { + if (!condition) std::fprintf(stderr, "%s\n", name); + return condition; +} + +bool run(const char* name, const std::function& test) { + try { + return expect(name, test()); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + return false; + } +} + +bool AllUsed(const std::vector& values) { + for (const auto* value : values) { + if (!value->HasUses()) return false; + } + return true; +} + +bool PureSpirv(std::uint32_t opcode) { + switch (opcode) { + case spv::OpBitcast: + case spv::OpUConvert: + case spv::OpSConvert: + case spv::OpFConvert: + case spv::OpConvertFToU: + case spv::OpConvertFToS: + case spv::OpConvertUToF: + case spv::OpConvertSToF: + case spv::OpCompositeConstruct: + case spv::OpCompositeExtract: + case spv::OpSNegate: + case spv::OpFNegate: + case spv::OpIAdd: + case spv::OpISub: + case spv::OpIMul: + case spv::OpFAdd: + case spv::OpFSub: + case spv::OpFMul: + case spv::OpShiftLeftLogical: + case spv::OpShiftRightLogical: + case spv::OpShiftRightArithmetic: + case spv::OpBitwiseAnd: + case spv::OpBitwiseOr: + case spv::OpBitwiseXor: + case spv::OpNot: + case spv::OpBitFieldInsert: + case spv::OpBitFieldUExtract: + case spv::OpBitFieldSExtract: + case spv::OpBitReverse: + case spv::OpBitCount: + case spv::OpSelect: + case spv::OpIEqual: + case spv::OpINotEqual: + case spv::OpULessThan: + case spv::OpULessThanEqual: + case spv::OpUGreaterThan: + case spv::OpUGreaterThanEqual: + case spv::OpSLessThan: + case spv::OpSLessThanEqual: + case spv::OpSGreaterThan: + case spv::OpSGreaterThanEqual: + case spv::OpFOrdEqual: + case spv::OpFOrdNotEqual: + case spv::OpFOrdLessThan: + case spv::OpFOrdGreaterThan: + case spv::OpFOrdLessThanEqual: + case spv::OpFOrdGreaterThanEqual: + case spv::OpFUnordEqual: + case spv::OpFUnordNotEqual: + case spv::OpFUnordLessThan: + case spv::OpFUnordGreaterThan: + case spv::OpFUnordLessThanEqual: + case spv::OpFUnordGreaterThanEqual: + case spv::OpIsNan: + case spv::OpLogicalAnd: + case spv::OpLogicalOr: + case spv::OpLogicalNot: + case spv::OpLogicalEqual: + case spv::OpLogicalNotEqual: + return true; + default: + return false; + } +} + +std::size_t RepeatedInBlock(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::size_t repeated = 0; + std::set> seen; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpLabel) seen.clear(); + if (PureSpirv(opcode) && count >= 3) { + std::vector key{opcode, words[offset + 1]}; + key.insert(key.end(), words.begin() + static_cast(offset + 3), words.begin() + static_cast(offset + count)); + if (!seen.insert(std::move(key)).second) ++repeated; + } + offset += count; + } + return repeated; +} + +std::vector Recompiled(std::span code) { + 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.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 = 32u; + 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.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +} + +int main() { + bool passed = true; + + passed &= run("a value recomputed from the same operands in one block is replaced by the first", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.flush(source); + auto& second = b.flush(source); + b.keep(first); + b.keep(second); + return b.eliminate() == 4u && !second.HasUses() && first.UseCount() == 2u; + }); + + passed &= run("values that differ in an immediate, an operand or the opcode stay", [] { + Builder b; + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& masked = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + auto& otherMask = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x80000000u)}); + auto& otherSource = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&other, &b.constant(0x7f800000u)}); + auto& otherOpcode = b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&source, &b.constant(0x7f800000u)}); + b.keep(masked); + b.keep(otherMask); + b.keep(otherSource); + b.keep(otherOpcode); + return b.eliminate() == 0u && AllUsed({&masked, &otherMask, &otherSource, &otherOpcode}); + }); + + passed &= run("values that differ only in flags, the result type or the type of an immediate stay", [] { + Builder b; + auto& source = b.unknown(); + auto& plain = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}); + auto& flagged = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.constant(0x3f800000u)}, 1u); + auto& otherType = b.emit(IrOpcode::BitwiseAnd32, IrType::F32, {&source, &b.constant(0x3f800000u)}); + auto& floatImmediate = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&source, &b.floatConstant(0x3f800000u)}); + b.keep(plain); + b.keep(flagged); + b.keep(otherType); + b.keep(floatImmediate); + return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate}); + }); + + 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()}); + auto& plusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x00000000u)}); + auto& minusZero = b.emit(IrOpcode::FPAdd32, IrType::F32, {&x, &b.floatConstant(0x80000000u)}); + auto& quiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& sameQuiet = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00000u)}); + auto& otherPayload = b.emit(IrOpcode::FPMax32, IrType::F32, {&x, &b.floatConstant(0x7fc00001u)}); + for (auto* value : {&plusZero, &minusZero, &quiet, &sameQuiet, &otherPayload}) b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {value})); + return b.eliminate() == 2u && !sameQuiet.HasUses() && AllUsed({&quiet, &plusZero, &minusZero, &otherPayload}); + }); + + passed &= run("a value recomputed in a dominated block is replaced, one in a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& source = b.unknown(); + auto& other = b.unknown(); + auto& first = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + b.keep(first); + b.block = &then; + auto& dominated = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&source}); + auto& thenOnly = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(dominated); + b.keep(thenOnly); + b.block = &otherwise; + auto& sibling = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(sibling); + b.block = &merge; + auto& merged = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&other}); + b.keep(merged); + return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); + }); + + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& source = b.unknown(); + auto& other = b.unknown(); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + b.keep(inHeader); + b.block = &body; + auto& inBody = b.emit(IrOpcode::IAdd32, IrType::U32, {&source, &b.constant(1u)}); + auto& bodyOnly = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(inBody); + b.keep(bodyOnly); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::IAdd32, IrType::U32, {&other, &b.constant(1u)}); + b.keep(afterLoop); + return b.eliminate() == 1u && !inBody.HasUses() && inHeader.UseCount() == 2u && AllUsed({&bodyOnly, &afterLoop}); + }); + + passed &= run("a branch condition that is replaced names the value that stays", [] { + Builder b; + auto& source = b.unknown(); + auto& first = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + auto& second = b.emit(IrOpcode::IEqual32, IrType::Bool, {&source, &b.constant(0u)}); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&first, &source, &b.constant(1u)})); + b.keep(b.emit(IrOpcode::SelectU32, IrType::U32, {&second, &b.constant(1u), &source})); + BlockInfo info; + info.condition = &second; + b.program.Metadata().blockInfo.push_back(info); + return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first; + }); + + passed &= run("lane reads are not merged", [] { + Builder b; + auto& first = b.unknown(); + auto& second = b.unknown(); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && second.HasUses(); + }); + + passed &= run("cross-lane reads with the same operands are not merged", [] { + Builder b; + auto& source = b.unknown(); + std::vector values; + for (int copy = 0; copy < 2; ++copy) { + values.push_back(&b.emit(IrOpcode::ReadFirstLane, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::ReadLane, IrType::U32, {&source, &b.constant(3u)})); + values.push_back(&b.emit(IrOpcode::DppMoveU32, IrType::U32, {&source, &b.truth()})); + values.push_back(&b.emit(IrOpcode::SwizzleU32, IrType::U32, {&source, &b.constant(0x1fu), &b.truth()})); + } + for (auto* value : values) b.keep(*value); + return b.eliminate() == 0u && AllUsed(values); + }); + + passed &= run("loads of one address are not merged, with no store, a store or a barrier between them", [] { + Builder b; + auto& address = b.unknown(); + std::vector loads; + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::StoreShared, IrType::Void, {&address, &b.constant(7u)})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + static_cast(b.emit(IrOpcode::Barrier, IrType::Void, {})); + loads.push_back(&b.emit(IrOpcode::LoadShared, IrType::U32, {&address})); + for (auto* load : loads) b.keep(*load); + return b.eliminate() == 0u && AllUsed(loads); + }); + + passed &= run("atomics with the same operands are not merged", [] { + Builder b; + auto& address = b.unknown(); + auto& first = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + auto& second = b.emit(IrOpcode::SharedAtomicIAdd32, IrType::U32, {&address, &b.constant(1u), &b.truth()}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("phis with the same incoming values are not merged", [] { + Builder b; + IrBlock& left = b.newBlock(); + IrBlock& right = b.newBlock(); + IrBlock& merge = b.newBlock(); + b.block->AddBranch(&left); + b.block->AddBranch(&right); + left.AddBranch(&merge); + right.AddBranch(&merge); + auto& one = b.unknown(); + auto& two = b.unknown(); + b.block = &merge; + auto& first = b.emit(IrOpcode::Phi, IrType::U32, {}); + auto& second = b.emit(IrOpcode::Phi, IrType::U32, {}); + for (auto* phi : {&first, &second}) { + phi->AddPhiOperand(&left, &one); + phi->AddPhiOperand(&right, &two); + } + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("float operations with the same operands in the same order are merged", [] { + Builder b; + auto& lhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& rhs = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()}); + auto& first = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& second = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& swapped = b.emit(IrOpcode::FPMul32, IrType::F32, {&rhs, &lhs}); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&first})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&second})); + b.keep(b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&swapped})); + return b.eliminate() == 2u && !second.HasUses() && swapped.HasUses(); + }); + + passed &= run("a block that the entry does not reach throws", [] { + Builder b; + static_cast(b.newBlock()); + try { + static_cast(b.eliminate()); + } catch (const std::runtime_error&) { + return true; + } + return false; + }); + + passed &= run("a wave64 shader that reads one register in several float operations computes nothing twice in a block", [] { + const std::array code{ + 0x10020100u, + 0x10040300u, + 0x10060500u, + 0x06020503u, + 0xe0700000u, 0x80000100u, + 0xbf810000u, + }; + const auto module = Recompiled(code); + const auto repeated = RepeatedInBlock(module); + if (repeated != 0u) std::fprintf(stderr, " %zu pure instructions repeat an earlier one in their block of the %zu-word module\n", repeated, module.size()); + return repeated == 0u; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp index 9efc3e9265..e52bb5b232 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ResidentPresent.cpp @@ -1,6 +1,7 @@ #include "ResidentPresent.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,11 +22,14 @@ namespace { using namespace AgcDriver::Graphics; using AgcDriver::DisplayBuffer; using AgcDriver::ResidentPresent; +using AgcDriver::ResidentPresentSlice; constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; constexpr std::uint64_t Rgba8 = 0x8000000022000000ull; constexpr std::uint64_t TenBit = 0x0100000000000000ull; constexpr std::uint64_t Rgba16Float = 0xc001000622000000ull; +constexpr std::uint64_t Rgb10Pq = 0x8100070422000000ull; +constexpr std::uint64_t Bgr10Pq = 0x8100070400000000ull; void decisionTests() { using AgcDriver::DisplayTexelFormat; @@ -41,6 +46,13 @@ void decisionTests() { } Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a 10-bit display"); } + Require(DisplayTexelFormat(Bgr10Pq) == VK_FORMAT_A2R10G10B10_UNORM_PACK32 && DisplayTexelFormat(Rgb10Pq) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "PQ display texel formats are wrong"); + for (const auto display : {Bgr10Pq, Rgb10Pq}) { + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) { + Require(ResidentPresentPath(storage, display, true) == ResidentPresent::Convert, "a PQ image is blitted (it must be converted to SDR)"); + } + Require(ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, display, true) == ResidentPresent::None, "an 8-bit image presents a PQ display"); + } Require(ResidentPresentPath(VK_FORMAT_A2B10G10R10_UNORM_PACK32, Bgra8, true) == ResidentPresent::None && ResidentPresentPath(VK_FORMAT_R16G16_SFLOAT, Bgra8, true) == ResidentPresent::None, "an image of another type presents an 8-bit display"); const auto guest = FindGuestColorTargetFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32, 4); Require(guest.has_value() && guest == FindGuestTextureFormat(VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4), "the blue-low 10-bit color target has no storage format"); @@ -53,7 +65,23 @@ std::uint32_t memoryType(const Context& context, std::uint32_t bits, VkMemoryPro throw std::runtime_error("no device-local memory type for the test image"); } -void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format) { +void sliceTests() { + constexpr std::uint64_t image = 0x208000000ull; + constexpr std::uint64_t slice = 0x1fe0000ull; + Require(ResidentPresentSlice(image, slice, 1, image, slice) == 0u, "a display buffer that is the whole image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image, slice) == 0u, "a display buffer at the start of a two-slice image is not its slice 0"); + Require(ResidentPresentSlice(image, 2 * slice, 2, image + slice, slice) == 1u, "a display buffer one slice into a two-slice image is not its slice 1"); + Require(ResidentPresentSlice(image, 6 * slice, 6, image + 5 * slice, slice) == 5u, "a display buffer at the last of six slices is not slice 5"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 2 * slice, slice), "a display buffer past the image's last slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image + 0x10000, slice), "a display buffer between two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image - slice, slice), "a display buffer before the image has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, 2 * slice), "a display buffer the size of two slices has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice, 2, image, slice / 2), "a display buffer of half a slice has a slice"); + Require(!ResidentPresentSlice(image, 2 * slice + 1, 2, image, slice), "an image whose bytes do not divide into its slices has a slice"); + Require(!ResidentPresentSlice(image, 0, 0, image, slice) && !ResidentPresentSlice(image, slice, 1, image, 0), "an empty image or display buffer has a slice"); +} + +void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat format, std::uint32_t layer = 0, float whiteNits = AgcDriver::SdrToneMapLuminanceNits) { constexpr std::uint32_t width = 200; constexpr std::uint32_t height = 130; const auto texelBytes = AgcDriver::DisplayTexelBytes(pixelFormat); @@ -76,14 +104,22 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat for (std::uint32_t x = 0; x < width; ++x) std::memcpy(guest.data() + AgcDriver::DisplayBufferOffset(display, x, y), words.data() + (static_cast(y) * width + x) * texelBytes, texelBytes); } } - const auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + auto expected = AgcDriver::DecodeDisplayBuffer(display, guest); + if (AgcDriver::DisplayPq(pixelFormat) && whiteNits != AgcDriver::SdrToneMapLuminanceNits) { + for (std::size_t i = 0; i < expected.size(); i += 4) { + std::uint32_t word = 0; + std::memcpy(&word, words.data() + i, 4); + const auto pixel = AgcDriver::PqToSdrPixel(word, AgcDriver::DisplayRedLow(pixelFormat), whiteNits); + for (std::size_t channel = 0; channel < pixel.size(); ++channel) expected[i + channel] = static_cast(pixel[channel]); + } + } VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.format = format; imageInfo.extent = {width, height, 1}; imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; + imageInfo.arrayLayers = layer + 1u; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; @@ -114,8 +150,10 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat Buffer fromGuest(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT); GpuColorTransfer transfer(context); const bool redLow = AgcDriver::DisplayRedLow(pixelFormat); - const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat); + const bool pq = AgcDriver::DisplayPq(pixelFormat); + const bool tenBit = AgcDriver::DisplayTenBit(pixelFormat) && !pq; const bool halfFloat = AgcDriver::DisplayHalfFloat(pixelFormat); + const auto pqWhiteNits = pq ? std::optional(whiteNits) : std::nullopt; const auto barrier = context.Function("vkCmdPipelineBarrier"); const auto readback = [&](VkCommandBuffer commands, Buffer& destination) { const VkBufferCopy copy{0, 0, expected.size()}; @@ -132,38 +170,55 @@ void conversionTest(const Context& context, std::uint64_t pixelFormat, VkFormat toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransfer.srcQueueFamilyIndex = toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransfer.image = image; - toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer + 1u}; barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toTransfer); VkBufferImageCopy region{}; - region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, layer, 1}; region.imageExtent = {width, height, 1}; context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + if (layer != 0) { + const VkClearColorValue other{}; + const VkImageSubresourceRange lower{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layer}; + context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &other, 1, &lower); + } auto toGeneral = toTransfer; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); - transfer.Detile(commands, redLow, tenBit, halfFloat); + transfer.Detile(commands, redLow, tenBit, halfFloat, pqWhiteNits); readback(commands, fromGuest); - transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat); - readback(commands, fromImage); + if (!pq) { + transfer.DetileImage(commands, image, VK_IMAGE_LAYOUT_GENERAL, width, height, ColorTileMode::RenderTarget, redLow, tenBit, halfFloat, layer); + readback(commands, fromImage); + } batch.SubmitAndWait(); } - const auto name = std::string(halfFloat ? "16-bit float " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from VkFormat " + std::to_string(static_cast(format)); - Require(std::equal(expected.begin(), expected.end(), fromGuest.Bytes().begin()), "the GPU detile differs from the CPU decode: " + name); - Require(std::equal(expected.begin(), expected.end(), fromImage.Bytes().begin()), "the resident image presents other pixels than guest memory: " + name); + const auto name = std::string(halfFloat ? "16-bit float " : pq ? "PQ " : tenBit ? "10-bit " : "8-bit ") + (redLow ? "red-low" : "blue-low") + " display from layer " + std::to_string(layer) + " of VkFormat " + std::to_string(static_cast(format)); + const auto matches = [&](std::span actual) { + return std::equal(expected.begin(), expected.end(), actual.begin(), [&](std::byte want, std::byte got) { return std::abs(std::to_integer(want) - std::to_integer(got)) <= (pq ? 1 : 0); }); + }; + Require(matches(fromGuest.Bytes()), "the GPU detile differs from the CPU decode: " + name); + Require(pq || matches(fromImage.Bytes()), "the resident image presents other pixels than guest memory: " + name); } } void RunResidentPresentTests(const Context& context) { decisionTests(); + sliceTests(); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Rgba8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32); conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2R10G10B10_UNORM_PACK32); conversionTest(context, Bgra8, VK_FORMAT_R8G8B8A8_UNORM); conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM); conversionTest(context, Rgba16Float, VK_FORMAT_R16G16B16A16_SFLOAT); + conversionTest(context, Rgba8, VK_FORMAT_B8G8R8A8_UNORM, 1); + conversionTest(context, Bgra8 | TenBit, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 2); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32); + conversionTest(context, Rgb10Pq, VK_FORMAT_A2B10G10R10_UNORM_PACK32, 0, 1000.0f); + conversionTest(context, Bgr10Pq, VK_FORMAT_A2R10G10B10_UNORM_PACK32, 0, 150.0f); std::cout << "resident present format and conversion tests passed\n"; } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6ab1458817..130508267e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -7,6 +7,9 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "ShaderCacheDirectory.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "Optimization/RequestMemoryView.hpp" +#include "Optimization/SrtWalker.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" #include #include #include @@ -18,7 +21,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -63,7 +69,7 @@ bool sameBindings(const std::vector& left, const std::vector< void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShaderArtifact& right, const char* what) { const std::string prefix = std::string(what) + ": "; - require(left.spirv.Words() == right.spirv.Words(), prefix + "SPIR-V differs"); + require(left.spirv == right.spirv, prefix + "SPIR-V differs"); require(left.bdaAbiVersion == right.bdaAbiVersion, prefix + "BDA ABI version differs"); require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs"); require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs"); @@ -247,6 +253,7 @@ CompiledVariant sampleVariant() { info.info.usesDma = true; info.info.usesFaultBuffer = true; info.info.rasterPrimitive = true; + info.info.layerExported = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; @@ -540,6 +547,16 @@ void verifyAtomicReplacement(const std::filesystem::path& directory) { const std::vector original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}}; const std::vector replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}}; require(WriteFileAtomically(path, original), "cannot write the initial cache entry"); +#ifdef _WIN32 + { + const auto renaming = CreateFileW(path.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + require(renaming != INVALID_HANDLE_VALUE, "cannot retain the cache entry's rename handle"); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{renaming}; + std::vector bytes; + require(ReadWholeFile(path, bytes) && bytes == original, "cache reader failed while the published entry retained its rename handle"); + } +#endif { Reader old(path); require(old.Read() == original, "the retained reader did not read the original entry"); @@ -588,6 +605,36 @@ void verifyStore() { require(ShaderDiskCache::Load(key, loaded), "a rewritten entry does not load"); } +void verifyPersist() { + SampleRequest sample; + const auto key = sample.Key(); + const auto variant = sampleVariant(); + const auto persisted = ShaderDiskCache::Persist(key, variant); + require(persisted != nullptr, "a persisted entry was not read back"); + const auto path = ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION region{}; + require(VirtualQuery(persisted->artifact.spirv.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "persisted SPIR-V is not a mapped cache entry"); + const auto close = [](void* handle) { CloseHandle(handle); }; + std::unique_ptr writer(CreateFileW(path.c_str(), GENERIC_WRITE | DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr), close); + require(writer.get() != INVALID_HANDLE_VALUE, "cannot retain the published cache writer"); + CompiledVariant loaded; + require(ShaderDiskCache::Load(key, loaded), "cannot load a cache entry while its writer retains rename access"); + const auto& loadedWords = loaded.artifact.spirv; + require(VirtualQuery(loadedWords.data(), ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "cache loading with a retained writer did not map the entry"); + writer.reset(); +#endif + requireSameVariant(variant, *persisted, "persist round trip"); + auto detached = persisted->artifact.spirv; + detached[0] ^= 1u; + require(detached[0] != persisted->artifact.spirv[0], "changing a copied mapped module changed its owner"); + require(WriteFileAtomically(path, std::vector(64, std::byte{0x5a})), "cannot replace the persisted entry"); + std::error_code error; + std::filesystem::remove(path, error); + require(!error, "cannot remove the replaced cache entry"); + requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed"); +} + struct ComputeRequest { std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; @@ -938,7 +985,7 @@ void verifyVertexTypeSpecialization() { } } require(checked == 4u, "vertex specialization lost attribute loads"); - require(artifact.spirv.Words() == original, "vertex specialization mutated the shared template"); + require(std::ranges::equal(artifact.spirv.Words(), original), "vertex specialization mutated the shared template"); } } auto invalid = artifact; @@ -1082,6 +1129,743 @@ void verifySpecializationLiveness() { require(SpecializeSpirv(specialized) == specialized, "dead computation removal is not stable"); } +constexpr std::uint64_t PlanSrtAddress = 0x7f0000010000ull; +constexpr std::uint64_t PlanTableAddress = 0x7f0000020000ull; +constexpr std::uint64_t PlanMaterialAddress = 0x7f0000030000ull; +constexpr std::uint64_t PlanPayloadAddress = 0x7f0000040000ull; +constexpr std::uint64_t PlanPointerAddress = 0x7f0000048000ull; +constexpr std::uint64_t PlanOutputAddress = 0x7f0000050000ull; +constexpr std::uint64_t PlanTextureAddress = 0x7f0000060000ull; +constexpr std::uint64_t PlanUnmappedAddress = 0x7f00000f0000ull; + +struct PlanFixture { + std::string name; + ShaderStage stage = ShaderStage::Compute; + std::uint64_t codeAddress = 0; + std::uint32_t waveSize = 64; + std::vector code; + std::vector userData; + std::vector>> regions; + std::optional pixel; + std::vector memory; + + RecompileRequest Request(bool useCache) { + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilitySampledImageArrayDynamicIndexing, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + memory.clear(); + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RecompileRequest request{}; + request.shader = {stage, codeAddress, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (stage == ShaderStage::Compute) request.context.compute = ShaderComputeStageInfo{{waveSize, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.pixel = pixel; + request.context.memory = memory; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = 1u; + request.layout.pushConstantSizeBytes = 128; + request.useCache = useCache; + return request; + } +}; + +std::vector imageWords(std::uint64_t base) { + return {static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; +} + +std::vector bufferWords(std::uint64_t base, std::uint32_t stride, std::uint32_t records, std::uint32_t format = 0xfacu) { + return {static_cast(base), static_cast((base >> 32u) & 0xffffu) | (stride << 16u), records, format}; +} + +void place(std::vector& words, std::size_t offset, const std::vector& values) { + std::copy(values.begin(), values.end(), words.begin() + static_cast(offset)); +} + +std::vector planFixtures() { + std::vector fixtures; + const std::vector srtPointer{static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u)}; + const auto add = [&](std::string name, std::uint32_t waveSize, std::vector code, std::vector userData, std::vector>> regions) { + PlanFixture fixture; + fixture.name = std::move(name); + fixture.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fixture.waveSize = waveSize; + fixture.code = std::move(code); + fixture.userData = std::move(userData); + fixture.regions = std::move(regions); + fixtures.push_back(std::move(fixture)); + }; + add("user data buffer", 64, {0xe0700000u, 0x80000000u, 0xbf810000u}, bufferWords(PlanOutputAddress, 0u, 0x40u, 0x00027facu), {}); + + std::vector guarded(32, 0u); + place(guarded, 0, imageWords(PlanTextureAddress)); + place(guarded, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + guarded[20] = 1u; + add("guarded image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + add("guarded image stores", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, guarded}}); + + std::vector heap(32, 0u); + place(heap, 0, imageWords(PlanTextureAddress)); + place(heap, 8, imageWords(PlanTextureAddress + 0x100u)); + place(heap, 24, imageWords(PlanTextureAddress)); + const std::vector materials{0u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 3u, 0u, 0u}; + std::vector tables(16, 0u); + place(tables, 0, bufferWords(PlanTableAddress, 32u, 4u)); + place(tables, 8, bufferWords(PlanMaterialAddress, 16u, 3u)); + place(tables, 12, bufferWords(PlanOutputAddress, 0u, 16u)); + add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + + std::vector phis(32, 0u); + place(phis, 0, imageWords(PlanTextureAddress)); + place(phis, 12, {0x49u, 0u, 0x00500000u, 0u}); + place(phis, 16, bufferWords(PlanOutputAddress, 0u, 16u)); + phis[20] = 1u; + place(phis, 24, imageWords(PlanTextureAddress + 0x100u)); + add("descriptor phi sampler", 32, {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi image", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi loop", 32, {0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbe910380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf068011u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf8cc07fu, 0x80118111u, 0xbf0a8211u, 0xbf85fff0u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + add("descriptor phi in sequence", 32, {0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0xfa000030u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + + std::vector root(16, 0u); + place(root, 0, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)); + root[4] = 1u; + root[6] = static_cast(PlanPayloadAddress); + root[7] = static_cast(PlanPayloadAddress >> 32u); + const std::vector payload{0x3f800000u}; + add("guarded nested read", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850003u, 0xf4000305u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, root}, {PlanPayloadAddress, payload}}); + auto chained = root; + chained[6] = static_cast(PlanPointerAddress); + chained[7] = static_cast(PlanPointerAddress >> 32u); + add("guarded pointer chain", 64, {0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0xbe8c03ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4040385u, 0xfa000000u, 0xbf8cc07fu, 0xf4000307u, 0xfa000000u, 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPayloadAddress, payload}, {PlanPointerAddress, {static_cast(PlanPayloadAddress), static_cast(PlanPayloadAddress >> 32u)}}}); + add("descriptor through a pointer", 64, {0xf4040280u, 0xfa000018u, 0xbf8cc07fu, 0xf4080105u, 0xfa000000u, 0xbf8cc07fu, 0x34040082u, 0xe0701000u, 0x80010002u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, chained}, {PlanPointerAddress, bufferWords(PlanOutputAddress, 0u, 256u, 0x30027facu)}}); + add("descriptor invariant phi", 32, {0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, phis}}); + + PlanFixture fragment; + fragment.name = "fragment parameters"; + fragment.stage = ShaderStage::Fragment; + fragment.codeAddress = 0x100000u + static_cast(fixtures.size()) * 0x1000u; + fragment.code = {0xc8100000u, 0xc8110001u, 0xc8140402u, 0xc8150403u, 0xf800180fu, 0x05040504u, 0xbf810000u}; + ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 2u; + pixel.interpolatorSettings[1] = 1u; + pixel.wave32 = true; + pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter); + pixel.hasPerspectiveCenterVgpr = true; + pixel.noPerspective = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping[0] = 0xe4u; + fragment.pixel = pixel; + fixtures.push_back(std::move(fragment)); + return fixtures; +} + +IrResourcePlan handBuiltPlan() { + IrResourcePlan plan; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + plan.valueStorage.push_back(std::make_unique(opcode, type, ids++)); + return *plan.valueStorage.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + auto& resource = make(IrOpcode::GetSrtResource, IrType::SrtResource); + const auto readConst = [&](std::uint32_t slot) -> IrValue& { + auto& read = make(IrOpcode::ReadConst, IrType::U32); + read.AddArgument(&resource); + read.AddArgument(&constant(slot)); + return read; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64); + handle.AddArgument(&userData(0)); + handle.AddArgument(&userData(1)); + auto& pointer = make(IrOpcode::LoadAddressU32, IrType::U32); + pointer.AddArgument(&handle); + pointer.AddArgument(&constant(8)); + plan.blockStorage.push_back(std::make_unique(3u)); + plan.blockStorage.push_back(std::make_unique(7u)); + auto& phi = make(IrOpcode::Phi, IrType::U32); + phi.AddPhiOperand(plan.blockStorage[0].get(), &readConst(0)); + phi.AddPhiOperand(plan.blockStorage[1].get(), &constant(0)); + auto& condition = make(IrOpcode::Void, IrType::Bool); + condition.SetImmediateU64(1u); + condition.SetFlags(std::uint64_t{0x1234u}); + plan.stage = IrShaderStage::Compute; + plan.shaderHash = 0x123456789abcull; + plan.userDataBase = 2u; + plan.userDataCount = 4u; + plan.srgbDecodeFormats = 5u; + MemoryInfo memory; + memory.kind = ResourceKind::Image; + memory.resource = 1u; + memory.dmask = 0xfu; + memory.imageDimension = RdnaImageDimension::Dim2D; + memory.gpuDescriptor = true; + memory.flushDenormals = true; + MemoryInfo every; + every.kind = ResourceKind::Buffer; + every.resource = 2u; + every.sampler = 3u; + every.offset = 0x14u; + every.secondaryOffset = 0x28u; + every.dmask = 0x5u; + every.dataDwords = 3u; + every.dataBits = 16u; + every.componentIndex = 1u; + every.componentCount = 2u; + every.dataFormat = 10u; + every.numberFormat = 7u; + every.imageSampleFlags = 0x21u; + every.imageDimension = RdnaImageDimension::Dim3D; + every.imageAddressComponents = 3u; + every.addressIsFull = true; + every.dataSigned = true; + every.typed = true; + every.formatted = true; + every.d16 = true; + every.imageHasMip = true; + every.imageR128 = true; + every.imagePacked = true; + every.imageByElements = 2u; + every.idxen = true; + every.offen = true; + every.planningOnly = true; + every.coherent = true; + every.gpuDescriptor = true; + every.flushDenormals = true; + plan.memoryInfo = {memory, every, MemoryInfo{}}; + DescriptorSource buffer; + buffer.dwordCount = 4u; + buffer.dwords = {&readConst(1), &readConst(2), &phi, &pointer}; + DescriptorSource table; + table.dwordCount = 8u; + table.dwords[0] = &readConst(3); + table.indirectImage = DescriptorSource::IndirectImage{3u, 2u, 16u, 4u, 2u, 32u, true}; + DescriptorSource heap; + heap.dwordCount = 4u; + heap.dwords[1] = &readConst(6); + DescriptorSource material; + material.dwordCount = 4u; + material.dwords[2] = &readConst(7); + auto& wide = make(IrOpcode::Void, IrType::U64); + 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.materializationSources = {0u, 2u}; + plan.srtReads = {{&readConst(4), 2u}, {&pointer, 1u}}; + plan.guardedSrtSlots = {1u}; + plan.srtGuardOffset = 6u; + plan.cleanFlatSlots = {0u, 1u}; + plan.pureFlatSlots = {1u, 0u}; + plan.requiresSpecializationMemory = true; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.info.buffers = {BufferResource{}, BufferResource{2u, 0x40u, 0x100u, 0u, true, true, true, true, true, 0x7u, true, 4u}}; + ImageResource image{}; + image.source = 1u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + plan.info.images = {image}; + ResourceMaterializer::PrepareImageModes(plan.info); + plan.uniformFill.fill = {UniformFillKind::Buffer, 1u, {4u, 8u, 12u}, 2u, 0x3f800000u}; + auto& fillValue = readConst(5); + plan.uniformFill.values = {&fillValue, &fillValue, nullptr, nullptr}; + return plan; +} + +std::vector encodedPlan(const IrResourcePlan& plan) { + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "a self-contained plan does not encode"); + return bytes; +} + +bool samePlan(const IrResourcePlan& left, const IrResourcePlan& right) { + if (left.valueStorage.size() != right.valueStorage.size() || left.blockStorage.size() != right.blockStorage.size()) return false; + std::map leftValues; + std::map rightValues; + std::map leftBlocks; + std::map rightBlocks; + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + leftValues.emplace(left.valueStorage[index].get(), index); + rightValues.emplace(right.valueStorage[index].get(), index); + } + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + leftBlocks.emplace(left.blockStorage[index].get(), index); + rightBlocks.emplace(right.blockStorage[index].get(), index); + } + const auto sameValue = [&](const IrValue* a, const IrValue* b) { + if (a == nullptr || b == nullptr) return a == b; + const auto foundA = leftValues.find(a); + const auto foundB = rightValues.find(b); + return foundA != leftValues.end() && foundB != rightValues.end() && foundA->second == foundB->second; + }; + const auto sameBlock = [&](const IrBlock* a, const IrBlock* b) { + const auto foundA = leftBlocks.find(a); + const auto foundB = rightBlocks.find(b); + return foundA != leftBlocks.end() && foundB != rightBlocks.end() && foundA->second == foundB->second; + }; + for (std::size_t index = 0; index < left.blockStorage.size(); ++index) { + if (left.blockStorage[index]->Id() != right.blockStorage[index]->Id()) return false; + } + for (std::size_t index = 0; index < left.valueStorage.size(); ++index) { + const auto& a = *left.valueStorage[index]; + const auto& b = *right.valueStorage[index]; + if (a.Opcode() != b.Opcode() || a.Type() != b.Type() || a.Id() != b.Id() || a.Flags() != b.Flags() || !(a.Register() == b.Register()) || a.HasImmediate() != b.HasImmediate() || (a.HasImmediate() && a.ImmediateU64() != b.ImmediateU64())) return false; + if (a.ArgumentCount() != b.ArgumentCount() || a.IsPhi() != b.IsPhi() || a.PhiBlockCount() != b.PhiBlockCount()) return false; + for (std::size_t operand = 0; operand < a.ArgumentCount(); ++operand) { + if (!sameValue(a.Argument(operand), b.Argument(operand))) return false; + } + for (std::size_t operand = 0; operand < a.PhiBlockCount(); ++operand) { + if (!sameBlock(a.PhiBlock(operand), b.PhiBlock(operand))) return false; + } + } + if (left.stage != right.stage || left.shaderHash != right.shaderHash || left.userDataBase != right.userDataBase || left.userDataCount != right.userDataCount || left.srgbDecodeFormats != right.srgbDecodeFormats) return false; + if (left.memoryInfo != right.memoryInfo || left.descriptorSources.size() != right.descriptorSources.size() || left.controlFlow.size() != right.controlFlow.size() || left.srtReads.size() != right.srtReads.size()) return false; + for (std::size_t index = 0; index < left.descriptorSources.size(); ++index) { + const auto& a = left.descriptorSources[index]; + const auto& b = right.descriptorSources[index]; + if (a.dwordCount != b.dwordCount || a.indirectImage != b.indirectImage) return false; + for (std::size_t dword = 0; dword < a.dwords.size(); ++dword) { + if (!sameValue(a.dwords[dword], b.dwords[dword])) return false; + } + } + 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; + } + 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; + } + for (std::size_t index = 0; index < left.uniformFill.values.size(); ++index) { + if (!sameValue(left.uniformFill.values[index], right.uniformFill.values[index])) return false; + } + return left.materializationSources == right.materializationSources && left.guardedSrtSlots == right.guardedSrtSlots && left.srtGuardOffset == right.srtGuardOffset && left.cleanFlatSlots == right.cleanFlatSlots && left.pureFlatSlots == right.pureFlatSlots && + left.requiresSpecializationMemory == right.requiresSpecializationMemory && left.srtPlanComplete == right.srtPlanComplete && left.resourceTrackingComplete == right.resourceTrackingComplete && left.info == right.info && left.uniformFill.fill == right.uniformFill.fill; +} + +struct MaterializedPlan { + bool ok = false; + std::string error; + ResourceSnapshot snapshot; + SrtReadTrace trace; + std::size_t leaf = 0; +}; + +MaterializedPlan materializePlan(const IrResourcePlan& plan, SrtRuntime runtime) { + MaterializedPlan outcome; + runtime.readTrace = &outcome.trace; + try { + ResourceMaterializer{}.Materialize(plan, runtime, outcome.snapshot); + outcome.ok = true; + } catch (const std::exception& error) { + outcome.error = error.what(); + } + outcome.leaf = static_cast(std::find_if(plan.valueStorage.begin(), plan.valueStorage.end(), [&](const auto& value) { return value.get() == outcome.trace.leaf; }) - plan.valueStorage.begin()); + return outcome; +} + +bool sameMaterialization(const MaterializedPlan& left, const MaterializedPlan& right) { + const auto& a = left.snapshot; + const auto& b = right.snapshot; + return left.ok == right.ok && left.error == right.error && a.buffers == b.buffers && a.images == b.images && a.samplers == b.samplers && a.flattenedSrt == b.flattenedSrt && a.userData == b.userData && a.uniformFill == b.uniformFill && a.srtPoison == b.srtPoison && a.nullRootReads == b.nullRootReads && + left.leaf == right.leaf && left.trace.leafSlot == right.trace.leafSlot && left.trace.leaves == right.trace.leaves && left.trace.otherReads == right.trace.otherReads; +} + +void mutateWords(std::vector& words, std::mt19937& random) { + static constexpr std::array addresses{PlanSrtAddress, PlanTableAddress, PlanMaterialAddress, PlanPayloadAddress, PlanPointerAddress, PlanOutputAddress, PlanTextureAddress, PlanUnmappedAddress}; + for (auto& word : words) { + switch (random() % 16u) { + case 0: word = static_cast(random()); break; + case 1: word = 0u; break; + case 2: { + const auto address = addresses[random() % addresses.size()] + (random() % 8u) * 4u; + word = random() % 2u == 0u ? static_cast(address) : static_cast(address >> 32u); + break; + } + case 3: word ^= 1u << (random() % 32u); break; + default: break; + } + } +} + +std::size_t verifyRoundTripRuntimes(const IrResourcePlan& plan, const IrResourcePlan& decoded, const PlanFixture& fixture, std::uint32_t seed, const std::string& what) { + std::mt19937 random(seed); + std::size_t succeeded = 0; + for (std::uint32_t run = 0; run < 1000u; ++run) { + auto userData = fixture.userData; + auto regions = fixture.regions; + if (userData.size() < plan.userDataCount) userData.resize(plan.userDataCount, 0u); + if (run != 0u) { + mutateWords(userData, random); + for (auto& region : regions) mutateWords(region.second, random); + } + std::vector memory; + for (const auto& [address, words] : regions) memory.push_back({address, std::as_bytes(std::span(words))}); + RequestMemoryView view(memory); + const auto runtime = view.MakeRuntime(userData, fixture.codeAddress); + const auto original = materializePlan(plan, runtime); + const auto loaded = materializePlan(decoded, runtime); + require(sameMaterialization(original, loaded), what + ": runtime " + std::to_string(run) + " materialized differently from the decoded plan (" + original.error + " / " + loaded.error + ")"); + if (original.ok) ++succeeded; + } + return succeeded; +} + +void verifyPlanEntryDamage(const std::vector& key, const IrResourcePlan& plan, const std::string& what) { + const auto file = ShaderDiskCache::EncodePlanEntry(key, plan); + require(!file.empty(), what + ": the plan entry was not encoded"); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlanEntry(file, key, decoded) == ShaderDiskCache::LoadStatus::Loaded && encodedPlan(decoded) == encodedPlan(plan), what + ": the plan entry does not decode to the plan"); + for (std::size_t size = 0; size < file.size(); size += size < 512 ? 1 : 37) { + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(std::span(file).first(size), key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry truncated to " + std::to_string(size) + " bytes is not rejected"); + } + for (std::size_t offset = 0; offset < file.size(); offset += offset < 512 ? 1 : 29) { + for (const auto bit : {std::byte{0x01}, std::byte{0x80}}) { + auto damaged = file; + damaged[offset] ^= bit; + IrResourcePlan partial; + require(ShaderDiskCache::DecodePlanEntry(damaged, key, partial) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with byte " + std::to_string(offset) + " flipped is not rejected"); + } + } + auto longer = file; + longer.push_back(std::byte{0}); + require(ShaderDiskCache::DecodePlanEntry(longer, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry with a trailing byte is not rejected"); + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(file, otherKey, decoded) == ShaderDiskCache::LoadStatus::KeyMismatch, what + ": a plan entry for another key loads"); + auto previousFormat = file; + previousFormat[4] = static_cast(ShaderDiskCache::FormatVersion - 1u); + require(ShaderDiskCache::DecodePlanEntry(previousFormat, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of the previous format loads"); + auto otherSource = file; + otherSource[8] ^= std::byte{1}; + require(ShaderDiskCache::DecodePlanEntry(otherSource, key, decoded) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry of another source version loads"); + CompiledVariant variant; + require(ShaderDiskCache::DecodeEntry(file, key, variant) == ShaderDiskCache::LoadStatus::Rejected, what + ": a plan entry decodes as a compiled variant"); +} + +void verifyPlanPayloadDamage(const IrResourcePlan& plan, const std::string& what) { + const auto bytes = encodedPlan(plan); + for (std::size_t size = 0; size < bytes.size(); ++size) { + IrResourcePlan partial; + require(!ShaderDiskCache::DecodePlan(std::span(bytes).first(size), partial), what + ": a plan truncated to " + std::to_string(size) + " bytes decodes"); + } + std::size_t rejected = 0; + for (std::size_t offset = 0; offset < bytes.size(); ++offset) { + for (const auto bit : {std::byte{0x01}, std::byte{0x40}}) { + auto damaged = bytes; + damaged[offset] ^= bit; + IrResourcePlan decoded; + if (!ShaderDiskCache::DecodePlan(damaged, decoded)) { + ++rejected; + continue; + } + std::vector again; + require(ShaderDiskCache::EncodePlan(decoded, again), what + ": a plan accepted with byte " + std::to_string(offset) + " flipped is not self-contained"); + } + } + require(rejected != 0u, what + ": no damaged plan was rejected"); +} + +void verifyMalformedPlans() { + const auto rejects = [](const std::string& what, const std::function& damage) { + auto plan = handBuiltPlan(); + damage(plan); + std::vector bytes; + require(ShaderDiskCache::EncodePlan(plan, bytes), "malformed plan: " + what + " did not encode"); + IrResourcePlan decoded; + require(!ShaderDiskCache::DecodePlan(bytes, decoded), "malformed plan: " + what + " decodes"); + }; + rejects("a branch with one successor", [](IrResourcePlan& plan) { plan.controlFlow[0].successors.pop_back(); }); + rejects("a successor past the blocks", [](IrResourcePlan& plan) { plan.controlFlow[1].successors[0] = 3u; }); + rejects("a block source past the sources", [](IrResourcePlan& plan) { plan.controlFlow[0].sources[0] = 4u; }); + rejects("a block image past the images", [](IrResourcePlan& plan) { plan.controlFlow[1].images[0] = 1u; }); + rejects("a materialized source past the sources", [](IrResourcePlan& plan) { plan.materializationSources.push_back(4u); }); + rejects("a buffer past the sources", [](IrResourcePlan& plan) { plan.info.buffers[0].source = 4u; }); + rejects("an image past the sources", [](IrResourcePlan& plan) { plan.info.images[0].source = 5u; }); + rejects("a descriptor of nine dwords", [](IrResourcePlan& plan) { plan.descriptorSources[1].dwordCount = 9u; }); + rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); + rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->heapSource = 4u; }); + rejects("an image table material past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].indirectImage->materialSource = 4u; }); + auto foreign = handBuiltPlan(); + IrValue stray(IrOpcode::Void, IrType::U32, 99u); + foreign.srtReads[0].value = &stray; + std::vector bytes; + require(!ShaderDiskCache::EncodePlan(foreign, bytes), "a plan reading a value it does not own was encoded"); + auto unblocked = handBuiltPlan(); + auto& phi = *unblocked.valueStorage.emplace_back(std::make_unique(IrOpcode::Phi, IrType::U32, 100u)); + phi.AddArgument(unblocked.valueStorage[0].get()); + unblocked.srtReads[0].value = φ + bytes.clear(); + require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded"); + auto emptied = handBuiltPlan(); + emptied.valueStorage.push_back(nullptr); + bytes.clear(); + require(!ShaderDiskCache::EncodePlan(emptied, bytes), "a plan with an empty value slot was encoded"); +} + +void verifyPlanRoundTrip() { + const auto handBuilt = handBuiltPlan(); + const auto handBytes = encodedPlan(handBuilt); + IrResourcePlan handDecoded; + require(ShaderDiskCache::DecodePlan(handBytes, handDecoded), "the hand-built plan does not decode"); + require(encodedPlan(handDecoded) == handBytes && handDecoded.valueStorage.size() == handBuilt.valueStorage.size() && handDecoded.blockStorage.size() == handBuilt.blockStorage.size(), "the hand-built plan changed in a round trip"); + const auto& phi = *handDecoded.descriptorSources[0].dwords[2]; + require(phi.IsPhi() && phi.PhiBlockCount() == 2u && phi.PhiBlock(0)->Id() == 3u && phi.PhiBlock(1)->Id() == 7u && phi.Argument(1)->ImmediateU32() == 0u, "the decoded phi lost its blocks or operands"); + require(handDecoded.uniformFill.values[0] == handDecoded.uniformFill.values[1] && handDecoded.uniformFill.values[0] != nullptr && handDecoded.uniformFill.values[2] == nullptr, "the decoded uniform fill lost its shared or null values"); + require(samePlan(handBuilt, handDecoded), "the decoded hand-built plan differs from the plan in a field or a value"); + PlanFixture handFixture; + handFixture.userData = {static_cast(PlanSrtAddress), static_cast(PlanSrtAddress >> 32u), 0u, 0u}; + std::vector handSrt(16, 0u); + place(handSrt, 0, bufferWords(PlanOutputAddress, 0u, 16u)); + handFixture.regions = {{PlanSrtAddress, handSrt}, {PlanPayloadAddress, {0x3f800000u}}}; + static_cast(verifyRoundTripRuntimes(handBuilt, handDecoded, handFixture, 7u, "hand-built plan")); + verifyPlanPayloadDamage(handBuilt, "hand-built plan"); + verifyMalformedPlans(); + + auto fixtures = planFixtures(); + std::vector> plans; + std::size_t index = 0; + for (auto& fixture : fixtures) { + auto request = fixture.Request(false); + const auto plan = GetResourcePlan(request); + plans.push_back(plan); + const auto bytes = encodedPlan(*plan); + IrResourcePlan decoded; + require(ShaderDiskCache::DecodePlan(bytes, decoded), fixture.name + ": the plan does not decode"); + require(encodedPlan(decoded) == bytes, fixture.name + ": the plan changed in a round trip"); + require(samePlan(*plan, decoded), fixture.name + ": the decoded plan differs from the plan in a field or a value"); + require(decoded.valueStorage.size() == plan->valueStorage.size() && decoded.blockStorage.size() == plan->blockStorage.size(), fixture.name + ": the decoded plan holds another graph"); + const auto succeeded = verifyRoundTripRuntimes(*plan, decoded, fixture, 0x5eedu + static_cast(index), fixture.name); + require(succeeded != 0u, fixture.name + ": no runtime materialized"); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, 32u, key); + if (index % 4u == 0u) verifyPlanEntryDamage(key, *plan, fixture.name); + std::cout << "plan round trip: " << fixture.name << ", " << plan->valueStorage.size() << " values, " << ShaderDiskCache::EncodePlanEntry(key, *plan).size() << " bytes per entry, " << succeeded << " of 1000 runtimes materialized\n"; + ++index; + } + const auto covers = [&](const std::function& predicate) { + return std::any_of(plans.begin(), plans.end(), [&](const std::shared_ptr& plan) { return predicate(*plan); }); + }; + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.indirectImage.has_value(); }); }), "no fixture plans an indirect image table"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value->IsPhi(); }); }), "no fixture plans a phi"); + require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; }); }), "no fixture plans a pure flat slot"); +} + +void verifyPlanKey() { + auto fixtures = planFixtures(); + auto& fixture = fixtures[1]; + const auto planKey = [&](const std::function& change, std::uint32_t hostSubgroupSize = 32u) { + auto request = fixture.Request(true); + change(request); + std::vector key; + ShaderDiskCache::BuildPlanKey(request, hostSubgroupSize, key); + return key; + }; + const auto key = planKey([](RecompileRequest&) {}); + require(planKey([](RecompileRequest&) {}) == key, "the plan key is not deterministic"); + std::vector variantKey; + ShaderDiskCache::BuildKey(fixture.Request(true), 32u, variantKey); + require(variantKey != key && ShaderDiskCache::EntryName(variantKey) != ShaderDiskCache::EntryName(key), "the plan and the compiled variant share an entry"); + require(planKey([](RecompileRequest& request) { request.layout = {1u, 4u, 16u, 64u}; }) == key, "the binding layout changed the plan key"); + require(planKey([](RecompileRequest& request) { request.shader.codeAddress += 0x1000u; request.context.memory = {}; }) == key, "the code address or the memory changed the plan key"); + auto userData = fixture.userData; + userData[0] ^= 0x100u; + require(planKey([&](RecompileRequest& request) { request.context.userData = userData; }) == key, "user data values changed the plan key"); + const auto original = fixture.code; + for (std::size_t word = 0; word < original.size(); ++word) { + for (std::uint32_t bit = 0; bit < 32u; ++bit) { + fixture.code = original; + fixture.code[word] ^= 1u << bit; + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores code word " + std::to_string(word) + " bit " + std::to_string(bit)); + } + } + fixture.code = original; + fixture.code.push_back(0xbf810000u); + require(planKey([](RecompileRequest&) {}) != key, "the plan key ignores an appended code word"); + fixture.code = original; + require(planKey([](RecompileRequest&) {}, 64u) != key, "the plan key ignores the host subgroup size"); + require(planKey([](RecompileRequest& request) { request.target.subgroupSize = 64u; }) != key, "the plan key ignores the target"); + require(planKey([](RecompileRequest& request) { request.target.srgbDecodeFormats = 2u; }) != key, "the plan key ignores the sRGB formats decoded in the shader"); + require(planKey([](RecompileRequest& request) { request.context.waveSize = 64u; request.context.compute->numThreads[0] = 64u; }) != key, "the plan key ignores the wave size"); + require(planKey([](RecompileRequest& request) { request.context.floatMode = ShaderFloatMode{0xc0u, true, false, false}; }) != key, "the plan key ignores the float mode"); +} + +void appendWords(std::vector& out, std::span words) { + const auto count = static_cast(words.size()); + const auto countBytes = std::as_bytes(std::span(&count, 1)); + out.insert(out.end(), countBytes.begin(), countBytes.end()); + const auto bytes = std::as_bytes(words); + out.insert(out.end(), bytes.begin(), bytes.end()); +} + +void appendSnapshot(std::vector& out, const ResourceCapture& capture) { + const auto& snapshot = capture.snapshot; + for (const auto* values : {&snapshot.buffers, &snapshot.images, &snapshot.samplers}) { + std::vector words; + for (const auto& value : *values) { + words.push_back(value.dwordCount); + words.insert(words.end(), value.dwords.begin(), value.dwords.end()); + } + appendWords(out, words); + } + appendWords(out, snapshot.flattenedSrt); + appendWords(out, snapshot.userData); + const auto& fill = snapshot.uniformFill; + appendWords(out, std::vector{static_cast(fill.kind), fill.resource, fill.groupStride[0], fill.groupStride[1], fill.groupStride[2], fill.words, fill.value, snapshot.nullRootReads}); + std::vector poison; + for (const auto& entry : snapshot.srtPoison) poison.insert(poison.end(), {entry.slot, entry.pc, static_cast(entry.address), static_cast(entry.address >> 32u)}); + appendWords(out, poison); + std::vector leaves{capture.readTrace.leafSlot}; + for (const auto& [slot, address] : capture.readTrace.leaves) leaves.insert(leaves.end(), {slot, static_cast(address), static_cast(address >> 32u)}); + appendWords(out, leaves); + std::vector reads; + for (const auto address : capture.readTrace.otherReads) reads.insert(reads.end(), {static_cast(address), static_cast(address >> 32u)}); + appendWords(out, reads); +} + +std::vector> preparedOutputs(std::vector& fixtures, std::vector>* plans = nullptr) { + std::vector> outputs; + for (auto& fixture : fixtures) { + const auto request = fixture.Request(true); + const auto handle = PrepareShader(request); + RequestMemoryView view(request.context.memory); + const auto capture = CaptureResources(request, view.MakeRuntime(request.context.userData, request.shader.codeAddress), *handle); + const auto result = MaterializeShader(request, *capture, *handle); + std::vector bytes; + appendWords(bytes, GetPreparedArtifact(*handle).spirv.Words()); + ShaderDiskCache::EncodeResult(*result, bytes); + appendSnapshot(bytes, *capture); + require(ShaderDiskCache::EncodePlan(*capture->plan, bytes), fixture.name + ": the captured plan does not encode"); + if (plans != nullptr) plans->push_back(capture->plan); + outputs.push_back(std::move(bytes)); + } + return outputs; +} + +std::filesystem::path planOutputPath(std::size_t index) { + return ShaderCacheDirectory() / "plan-outputs" / ("fixture-" + std::to_string(index) + ".bin"); +} + +std::filesystem::path planEntryPath(PlanFixture& fixture) { + std::vector key; + ShaderDiskCache::BuildPlanKey(fixture.Request(true), 32u, key); + return ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(key); +} + +int runPlanLoadingProcess(std::string_view mode) { + if (mode == "--load-plan-switch") setEnvironment("APS5_TRACE_BINDLESS", "1"); + if (mode == "--load-plan-off") setEnvironment("APS5_NO_PERF_DISK_PLANS", "1"); + auto fixtures = planFixtures(); + const auto count = fixtures.size(); + std::vector> plans; + const auto outputs = preparedOutputs(fixtures, &plans); + ShaderDiskCache::Flush(); + const auto totals = ShaderDiskCache::Totals(); + const auto builds = FrontEndBuilds(); + const auto counts = " (front end builds " + std::to_string(builds) + ", plan hits " + std::to_string(totals.planHits) + ", plan misses " + std::to_string(totals.planMisses) + ", plan load failures " + std::to_string(totals.planLoadFailures) + ", variant hits " + std::to_string(totals.hits) + ", variant misses " + std::to_string(totals.misses) + ")"; + if (mode == "--load-plan-switch") { + require(builds == count && totals.planHits == 0u && totals.planMisses == count && totals.misses == count, "another switch value reused stored plans or variants" + counts); + std::cout << "a process with another switch value rebuilt all " << count << " plans\n"; + return 0; + } + if (mode == "--load-plan") { + require(builds == 0u && totals.planHits == count && totals.planMisses == 0u && totals.planLoadFailures == 0u && totals.hits == count && totals.misses == 0u, "the second process ran the front end" + counts); + } else if (mode == "--load-plan-off") { + require(builds == count && totals.planHits == 0u && totals.planMisses == 0u && totals.planWrites == 0u && totals.hits == count && totals.misses == 0u, "APS5_NO_PERF_DISK_PLANS=1 used the stored plans or missed the stored variants" + counts); + } else { + require(builds == 1u && totals.planHits == count - 1u && totals.planLoadFailures == 1u && totals.planWrites == 1u && totals.hits == count && totals.misses == 0u, "the damaged plan was not rejected and rebuilt alone" + counts); + } + for (std::size_t index = 0; index < count; ++index) { + std::vector expected; + require(ReadWholeFile(planOutputPath(index), expected), "cannot read the first process's output for " + fixtures[index].name); + require(outputs[index] == expected, fixtures[index].name + ": the SPIR-V, bindings, push constants, snapshot or plan differ from the first process"); + } + if (mode == "--load-plan") { + auto changed = planFixtures(); + auto& fixture = changed.front(); + fixture.code.insert(fixture.code.end() - 1, 0xbf800000u); + const auto request = fixture.Request(true); + static_cast(PrepareShader(request)); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() == 1u && after.planHits == count && after.planMisses == 1u, "a changed code word reused a stored plan"); + std::cout << "second process prepared " << count << " shaders from stored plans without running the front end\n"; + } else if (mode == "--load-plan-off") { + std::cout << "APS5_NO_PERF_DISK_PLANS=1 ran the front end for all " << count << " shaders with the same output\n"; + } else { + std::cout << "a damaged plan entry was rejected, rebuilt and stored again with the same output\n"; + } + for (std::size_t index = 0; index < count; ++index) { + const auto fresh = GetResourcePlan(fixtures[index].Request(false)); + require(samePlan(*plans[index], *fresh), fixtures[index].name + ": the plan this process used differs from the plan its front end builds"); + } + ShaderDiskCache::Flush(); + return 0; +} + +void verifyPlanAcrossProcesses(const char* self) { + auto fixtures = planFixtures(); + const auto builds = FrontEndBuilds(); + const auto before = ShaderDiskCache::Totals(); + const auto outputs = preparedOutputs(fixtures); + ShaderDiskCache::Flush(); + const auto after = ShaderDiskCache::Totals(); + require(FrontEndBuilds() - builds == fixtures.size() && after.planMisses - before.planMisses == fixtures.size() && after.planWrites - before.planWrites == fixtures.size(), "the first process did not build and store every plan"); + std::filesystem::create_directories(planOutputPath(0).parent_path()); + for (std::size_t index = 0; index < outputs.size(); ++index) require(WriteFileAtomically(planOutputPath(index), outputs[index]), "cannot write the first process's output"); + auto refused = fixtures[1]; + refused.codeAddress = 0x200000u; + refused.code = {0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + bool threw = false; + try { + static_cast(PrepareShader(refused.Request(true))); + } catch (const std::runtime_error&) { + threw = true; + } + ShaderDiskCache::Flush(); + require(threw && ShaderDiskCache::Totals().planWrites == after.planWrites && !std::filesystem::exists(planEntryPath(refused)), "a plan the front end refused was stored"); + std::uintmax_t planBytes = 0; + for (auto& fixture : fixtures) planBytes += std::filesystem::file_size(planEntryPath(fixture)); + std::cout << fixtures.size() << " plan entries, " << planBytes / fixtures.size() << " bytes per entry on average\n"; + const auto run = [&](const char* mode) { + const std::string command = "\"" + std::string(self) + "\" " + mode; + require(std::system(command.c_str()) == 0, std::string("the process ") + mode + " failed"); + }; + run("--load-plan"); + run("--load-plan-off"); + std::vector entry; + const auto damaged = planEntryPath(fixtures.front()); + require(ReadWholeFile(damaged, entry), "cannot read a stored plan entry"); + entry[entry.size() / 2] ^= std::byte{0x20}; + require(WriteFileAtomically(damaged, entry), "cannot damage a stored plan entry"); + run("--load-plan-corrupt"); + std::vector rewritten; + std::vector key; + ShaderDiskCache::BuildPlanKey(fixtures.front().Request(true), 32u, key); + IrResourcePlan plan; + require(ReadWholeFile(damaged, rewritten) && ShaderDiskCache::DecodePlanEntry(rewritten, key, plan) == ShaderDiskCache::LoadStatus::Loaded, "the rebuilt plan was not stored again"); + run("--load-plan-switch"); +} + void verifyDefaultDirectory(const char* self) { setEnvironment("ANYPS5_SHADER_CACHE_DIR", ""); setEnvironment("ANYPS5_NO_SHADER_CACHE", "1"); @@ -1099,6 +1883,7 @@ int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); + if (argc == 2 && std::string_view(argv[1]).starts_with("--load-plan")) return runPlanLoadingProcess(argv[1]); const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}())); std::filesystem::remove_all(directory); verifyDefaultDirectory(argv[0]); @@ -1110,8 +1895,12 @@ int main(int argc, char** argv) { verifyArtifactStorageIsolation(); verifyKeySensitivity(); verifyFloatModeSerialization(); + verifyPlanRoundTrip(); + verifyPlanKey(); verifyStore(); + verifyPersist(); verifyAcrossProcesses(argv[0]); + verifyPlanAcrossProcesses(argv[0]); verifyEmissionFailureMemo(); verifyFailureMemoSwitch(argv[0]); verifyInvocationIsolation(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6e412161ac..9021647520 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -18,10 +18,12 @@ #include "SpirvBackend/SpirvOptimizer.hpp" #endif #include "CacheKey.hpp" +#include "CompiledVariant.hpp" #include "BdaAbi.hpp" #include #include #include +#include #include #include #include @@ -338,6 +340,140 @@ void verifyFrontendPair() { } } +ShaderRecompiler::RecompileRequest LayoutPairRequest(std::span code, std::span userData, bool registration, std::uint32_t wordLimit) { + using namespace ShaderRecompiler; + auto request = FrontendRequest(code, userData); + request.shader.stage = ShaderStage::Fragment; + request.context.compute.reset(); + request.context.pixel = ShaderPixelStageInfo{}; + request.context.pixel->wave32 = true; + request.context.pixel->targetOutputMode[0] = 9u; + request.context.waveSize = 32u; + request.target.registrationModules = registration; + request.target.registrationWordLimit = wordLimit; + request.useCache = true; + return request; +} + +void verifyLayoutPairVariant(const ShaderRecompiler::SourceHandle& paired, const ShaderRecompiler::SourceHandle& isolated) { + const auto& left = *paired.artifact; + const auto& right = *isolated.artifact; + require(left.artifact.spirv == right.artifact.spirv, "layout pair: the SPIR-V differs from an isolated compile"); + require(left.bindings.layout == right.bindings.layout && left.bindings.layout.UsesPushData() == right.bindings.layout.UsesPushData() && left.bindings.pushConstantOffsetBytes == right.bindings.pushConstantOffsetBytes && left.bindings.pushConstantSizeBytes == right.bindings.pushConstantSizeBytes, "layout pair: the binding layout or push range differs from an isolated compile"); + const auto& a = left.artifact; + const auto& b = right.artifact; + const auto sameParameter = [](const ShaderRecompiler::FragmentParameter& p, const ShaderRecompiler::FragmentParameter& q) { + return p.location == q.location && p.sourceLocation == q.sourceLocation && p.flat == q.flat && p.perVertex == q.perVertex && p.custom == q.custom && p.flatP10 == q.flatP10; + }; + require(a.vertexInputPatches == b.vertexInputPatches && a.memoryOffsetDword == b.memoryOffsetDword && a.shaderDataDwords == b.shaderDataDwords && a.imageMetadataDword == b.imageMetadataDword && a.runtimeImageCount == b.runtimeImageCount && a.runtimeImageResources == b.runtimeImageResources && a.registrationModule == b.registrationModule && a.bdaAbiVersion == b.bdaAbiVersion && a.runtimeAbiVersion == b.runtimeAbiVersion, "layout pair: the artifact's bindings or ABI differ from an isolated compile"); + require(a.vertexInputs == b.vertexInputs && a.vertexOffsetSgpr == b.vertexOffsetSgpr && a.instanceOffsetSgpr == b.instanceOffsetSgpr && a.vertexOffsetShared == b.vertexOffsetShared && a.instanceOffsetShared == b.instanceOffsetShared && a.vertexOffsetConflict == b.vertexOffsetConflict && a.instanceOffsetConflict == b.instanceOffsetConflict && a.hostSubgroupSize == b.hostSubgroupSize, "layout pair: the artifact's vertex interface differs from an isolated compile"); + require(a.parameterExports == b.parameterExports && std::ranges::equal(a.fragmentParameters, b.fragmentParameters, sameParameter) && a.barycentricEmulation.active == b.barycentricEmulation.active && a.barycentricEmulation.smooth == b.barycentricEmulation.smooth && a.barycentricEmulation.linear == b.barycentricEmulation.linear, "layout pair: the artifact's fragment interface differs from an isolated compile"); + const auto& x = left.info; + const auto& y = right.info; + require(x.stage == y.stage && x.shaderHash == y.shaderHash && x.waveSize == y.waveSize && x.userDataBase == y.userDataBase && x.userDataCount == y.userDataCount && x.scratchDwords == y.scratchDwords && x.paramExportMask == y.paramExportMask && x.info == y.info && x.bindings == y.bindings, "layout pair: the compiled shader information differs from an isolated compile"); +} + +void verifyLayoutPairIsolation(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle& handle, std::uint32_t separateLimit) { + using namespace ShaderRecompiler; + const std::vector isolatedCode(request.shader.code.begin(), request.shader.code.end()); + auto isolated = request; + isolated.shader.code = isolatedCode; + isolated.target.registrationWordLimit = separateLimit; + const auto reference = PrepareShader(isolated); + require(reference->source != handle.source, "layout pair: the isolated compile shared a source"); + verifyLayoutPairVariant(handle, *reference); + SrtRuntime runtime{}; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + auto uncached = request; + uncached.useCache = false; + verifyResult(Recompile(uncached), *MaterializeShader(request, *CaptureResources(request, runtime, handle), handle)); +} + +void verifyLayoutPair() { + using namespace ShaderRecompiler; + const bool reuse = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; + const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; + const auto pixelCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0u, 0xbf810000u}; + }; + const auto interpolatedCode = [](std::uint32_t variant) { + return std::vector{0xbf800000u | variant, 0xc8100000u, 0xc8110001u, 0xc8140500u, 0xc8150501u, 0xf4200100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xf800180fu, 0x05040500u, 0xbf810000u}; + }; + const auto defaultLimit = SpirvTarget{}.registrationWordLimit; + struct Configuration { + bool registration; + std::uint32_t wordLimit; + std::uint32_t pushOffset; + bool interpolated; + }; + std::uint32_t variant = 0u; + for (const auto& [registration, wordLimit, pushOffset, interpolated] : {Configuration{false, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 0u, false}, Configuration{true, defaultLimit, 128u, false}, Configuration{true, 1u, 128u, false}, Configuration{true, defaultLimit, 128u, true}}) { + for (const bool zeroFirst : {false, true}) { + const auto code = interpolated ? interpolatedCode(variant++) : pixelCode(variant++); + auto pixel = LayoutPairRequest(code, buffer, registration, wordLimit); + pixel.layout.pushConstantOffsetBytes = pushOffset; + if (interpolated) { + pixel.context.pixel->interpolatorCount = 2u; + pixel.context.pixel->interpolatorSettings[1] = 1u; + pixel.context.pixel->inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + pixel.context.pixel->hasPerspectiveCenterVgpr = true; + } + auto zero = pixel; + zero.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(zeroFirst ? zero : pixel, &preparation); + const auto second = PrepareShader(zeroFirst ? pixel : zero, &preparation); + require(first->source == second->source, "layout pair: the two layouts did not share a source"); + require(preparation.ResourcePrograms() == (reuse ? 1u : 2u), "layout pair: the second layout ignored the reuse switch"); + const auto& pixelHandle = zeroFirst ? second : first; + const auto& zeroHandle = zeroFirst ? first : second; + require(pixelHandle->artifact->bindings.layout.UsesPushData() && !zeroHandle->artifact->bindings.layout.UsesPushData(), "layout pair: the fixture does not distinguish the push-constant capacities"); + const bool module = registration && wordLimit == defaultLimit; + require(GetPreparedArtifact(*pixelHandle).registrationModule == module && GetPreparedArtifact(*zeroHandle).registrationModule == module, "layout pair: the word limit did not choose the module mode"); + require(GetPreparedArtifact(*pixelHandle).fragmentParameters.size() == (interpolated ? 2u : 0u) && GetPreparedArtifact(*zeroHandle).fragmentParameters.size() == (interpolated ? 2u : 0u), "layout pair: the interpolated fixture did not declare its two parameters"); + verifyLayoutPairIsolation(pixel, *pixelHandle, wordLimit + 1u); + verifyLayoutPairIsolation(zero, *zeroHandle, wordLimit + 2u); + } + } + + const auto code = pixelCode(variant++); + const auto pixel = LayoutPairRequest(code, buffer, true, defaultLimit); + auto relocated = pixel; + relocated.shader.codeAddress += 0x1000u; + relocated.layout.pushConstantSizeBytes = 0u; + ShaderPreparationContext preparation; + const auto first = PrepareShader(pixel, &preparation); + const auto moved = PrepareShader(relocated, &preparation); + require(first->source == moved->source, "layout pair: relocated code did not share the source"); + require(preparation.ResourcePrograms() == 2u && moved->artifact->info.shaderHash == relocated.shader.codeAddress, "layout pair: a program translated at another code address was reused"); + verifyLayoutPairIsolation(relocated, *moved, defaultLimit + 1u); + + const std::vector colorCode{0xbf800000u | variant++, 0x7e000200u, 0xf800180fu, 0u, 0xbf810000u}; + auto color = LayoutPairRequest(colorCode, std::span(buffer).first(1u), true, defaultLimit); + color.shader.codeAddress = relocated.shader.codeAddress; + color.layout.pushConstantSizeBytes = 0u; + require(ResolveSource(color) != nullptr, "layout pair: the other source was not resolved"); + const auto colored = PrepareShader(color, &preparation); + require(colored->source != first->source && preparation.ResourcePrograms() == 2u, "layout pair: another source at the same code address reused the retained program"); + verifyLayoutPairIsolation(color, *colored, defaultLimit + 1u); + + const auto retained = colored->source.use_count(); + const auto otherCode = pixelCode(variant++); + static_cast(PrepareResourceProgram(LayoutPairRequest(otherCode, buffer, true, defaultLimit), &preparation)); + require(preparation.ResourcePrograms() == 3u && retained - colored->source.use_count() == (reuse ? 1 : 0), "layout pair: the context kept a program while it built another"); + + const std::vector computeCode{0xbf800000u, 0xbf810000u}; + auto compute = FrontendRequest(computeCode, buffer); + compute.context.userDataBaseRegister = 2u; + compute.context.compute->scratchDwords = 64u; + auto program = PrepareResourceProgram(compute); + const auto copied = program.CompiledInfo(); + const auto taken = std::move(program).TakeCompiledInfo(); + require(taken.userDataBase == 2u && taken.scratchDwords == 64u, "layout pair: the compute fixture did not set a user-data base and scratch"); + require(copied.stage == taken.stage && copied.shaderHash == taken.shaderHash && copied.waveSize == taken.waveSize && copied.userDataBase == taken.userDataBase && copied.userDataCount == taken.userDataCount && copied.scratchDwords == taken.scratchDwords && copied.paramExportMask == taken.paramExportMask && copied.info == taken.info && copied.bindings == taken.bindings, "layout pair: the copied compiled information differs from the moved one"); +} + void verifyRegisterSources() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -750,7 +886,7 @@ void verifyBindlessTable() { const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; // A wave64 workgroup on a 32-wide host is held by one subgroup (two lanes per invocation). - const auto makeRequest = [&](const std::vector& code) { + const auto makeRequest = [&](std::span code) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 64; @@ -843,7 +979,7 @@ void verifyBindlessTable() { bool nonUniform = false; bool switched = false; }; - const auto scan = [&](const std::vector& words) { + const auto scan = [&](std::span words) { SpirvScan result; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -923,6 +1059,24 @@ void verifyBindlessTable() { latch.context.memory = latchRegions; require(!Recompile(latch, *latchCapture)->spirv.empty(), "bindless: a sample in a loop latch did not compile"); + std::vector queryCode(wholeCode.begin(), wholeCode.begin() + 8); + queryCode.insert(queryCode.end(), {0xbea80380u, 0x7e040280u, 0x7e200500u, 0x80102810u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0380f08u, 0x00050402u, 0xf09c0f08u, 0x00450800u, 0x80288128u, 0xbf0a8228u, 0xbf85fff4u, 0xe0700000u, 0x80070800u, 0xe0700004u, 0x80070400u, 0xbf810000u}); + auto query = makeRequest(queryCode); + const auto queryPlan = GetResourcePlan(query); + require(std::ranges::count_if(queryPlan->descriptorSources, [](const DescriptorSource& source) { return source.indirectImage.has_value(); }) == 1, "bindless: a T# both queried and sampled was not planned as one table"); + AgcDriver::ShaderMemory queryMemory({}); + const auto queryCapture = queryMemory.Capture(query); + const auto queryDirect = static_cast(queryPlan->info.images.size()); + require(queryDirect == 1u, "bindless: the queried and sampled T# became more than one image"); + const auto queryRoot = tableRoot(*queryCapture, queryDirect); + require(queryCapture->snapshot.images[queryRoot].dwords == heap[0] && queryCapture->snapshot.images[queryDirect].dwords == heap[1], "bindless: the queried and sampled table slots are wrong"); + query.context.memory = queryMemory.Regions(); + const auto queried = Recompile(query, *queryCapture); + require(!queried->spirv.empty(), "bindless: a T# both queried and sampled did not compile"); +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ValidateAndOptimizeSpirv(queried->spirv, query.target.vulkanVersion, query.target.spirvVersion)); +#endif + // A table wider than the slots without a material pattern is rejected. fillSrt(100u); AgcDriver::ShaderMemory wideMemory({}); @@ -976,7 +1130,7 @@ void verifyDescriptorPhis() { const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - const auto makeRequest = [&](const std::vector& code, std::uint32_t waveSize = 32u) { + const auto makeRequest = [&](std::span code, std::uint32_t waveSize = 32u) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x21000u, code, 0, {}}; request.context.waveSize = waveSize; @@ -993,7 +1147,7 @@ void verifyDescriptorPhis() { request.layout.pushConstantSizeBytes = 128; return request; }; - const auto countOps = [](const std::vector& words, std::uint32_t opcode) { + const auto countOps = [](std::span words, std::uint32_t opcode) { std::size_t count = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto length = words[cursor] >> 16u; @@ -1009,7 +1163,7 @@ void verifyDescriptorPhis() { const std::vector imageCode{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf40c0200u, 0xfa000000u, 0xbf820002u, 0xf40c0200u, 0xfa000060u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; const std::vector dynamicCode{0xf40c0200u, 0xfa000000u, 0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850003u, 0xf4080500u, 0xfa000020u, 0xbf820002u, 0xf4080500u, 0x20000000u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, std::size_t images, std::size_t samplers) { + const auto compile = [&](std::span code, std::size_t images, std::size_t samplers) { auto request = makeRequest(code); const auto plan = GetResourcePlan(request); 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"); @@ -1092,7 +1246,7 @@ void verifyDescriptorPhis() { const std::array prologue{0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu}; const std::array epilogue{0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; - const auto planImages = [&](const std::vector& body) { + const auto planImages = [&](std::span body) { std::vector code(prologue.begin(), prologue.end()); code.insert(code.end(), body.begin(), body.end()); code.insert(code.end(), epilogue.begin(), epilogue.end()); @@ -1120,6 +1274,63 @@ void verifyDescriptorPhis() { expectFailure([&] { static_cast(planImages(splitReloads(260u))); }, "GetImageResource dword 0 is not a valid runtime value", "descriptor Phi: a web over more than 512 descriptor tuples was accepted"); } +void verifyUniformTableImage() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture textures[2]; + struct alignas(4096) GuestTables { + std::array, 3> table{}; + std::array output{}; + std::array srt{}; + }; + static GuestTables guest; + const auto texture = [](const Texture& image) { + const auto base = static_cast(reinterpret_cast(image.bytes.data())); + return std::array{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + }; + const auto first = texture(textures[0]); + const auto second = texture(textures[1]); + std::copy(first.begin(), first.end(), guest.table[1].begin() + 4); + std::copy(second.begin(), second.end(), guest.table[2].begin() + 4); + const auto bufferDescriptor = [](const void* base, std::uint32_t records) { + const auto address = static_cast(reinterpret_cast(base)); + return std::array{static_cast(address), static_cast((address >> 32u) & 0xffffu), records, 0xfacu}; + }; + const auto tableV = bufferDescriptor(guest.table.data(), static_cast(sizeof(guest.table))); + const auto outputV = bufferDescriptor(guest.output.data(), 16u); + std::copy(tableV.begin(), tableV.end(), guest.srt.begin()); + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + const auto srtAddress = static_cast(reinterpret_cast(guest.srt.data())); + std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u), 2u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x9310b002u, 0xf42c0502u, 0x20000010u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 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 = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && !plan->descriptorSources.at(plan->info.images[0].source).indirectImage.has_value(), "uniform table image: the T# read from a constant buffer was not planned as one image"); + for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { + userData[2] = key; + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(expected.begin(), expected.end(), capture->snapshot.images[0].dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + } +} + void verifyLaneSelectedDescriptors() { std::vector code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf40c0800u, 0xfa000060u, 0xbf8cc07fu, 0x7d880090u, 0xbf068010u, 0xbf850011u}; for (std::uint32_t dword = 0; dword < 8u; dword++) { @@ -1305,14 +1516,14 @@ void verifyHalfWaveReduction() { request.useCache = false; return Recompile(request).spirv; }; - const auto reductions = [](const std::vector& spirv, spv::Op opcode) { + const auto reductions = [](std::span spirv, spv::Op opcode) { std::uint32_t count = 0; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == static_cast(opcode) && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; } return count; }; - const auto halves = [](const std::vector& spirv, spv::Op opcode, std::uint32_t identity) { + const auto halves = [](std::span spirv, spv::Op opcode, std::uint32_t identity) { std::map definitions; std::vector reduces; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { @@ -1370,6 +1581,20 @@ void verifyHalfWaveReduction() { {0u, 0x38000000u, 0x88000000u, spv::OpGroupNonUniformBitwiseOr, "half-wave reduction: the wave64 OR scan did not read lane 31 as a subgroup OR on 32 lanes"}, }}; for (const auto& reduction : scans) require(reductions(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 32u, true), reduction.reduce) == 1u, reduction.message); + auto comparedScan = scan(0u, 0x38000000u, 0x88000000u); + *std::find(comparedScan.begin(), comparedScan.end(), 0xbe98047eu) = 0x8a986a7eu; + comparedScan.insert(comparedScan.begin(), 0x7c080080u); + require(reductions(pixel(comparedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by the lanes that failed a compare did not read lane 31 as a subgroup OR on 32 lanes"); + auto narrowedScan = comparedScan; + std::vector narrowing; + for (std::uint32_t step = 1; step <= 600u; ++step) { + const auto threshold = -static_cast(step); + std::uint32_t bits = 0; + std::memcpy(&bits, &threshold, sizeof(bits)); + narrowing.insert(narrowing.end(), {0x7c0800ffu, bits, 0x87fe6a7eu}); + } + narrowedScan.insert(narrowedScan.begin(), narrowing.begin(), narrowing.end()); + require(reductions(pixel(narrowedScan, 32u, true), spv::OpGroupNonUniformBitwiseOr) == 1u, "half-wave reduction: an OR scan keyed by lanes that 600 compares narrowed exec to did not read lane 31 as a subgroup OR on 32 lanes"); for (const auto& reduction : scans) { const auto wide = "half-wave reduction: a wave64 scan reduced by SPIR-V opcode " + std::to_string(reduction.reduce) + " did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"; require(halves(pixel(scan(reduction.identity, reduction.vector, reduction.scalar), 64u, true), reduction.reduce, reduction.identity), wide.c_str()); @@ -1391,6 +1616,25 @@ void verifyHalfWaveReduction() { }; require(reductions(vertex(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 vertex UMin scan, whose entry EXEC is every invocation, did not read lane 31 as a subgroup UMin on 32 lanes"); require(halves(vertex(scan(0x80000000u, 0x24000000u, 0x84000000u), 64u), spv::OpGroupNonUniformSMax, 0x80000000u), "half-wave reduction: the wave64 vertex SMax scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); + const auto domain = [](std::vector code, std::uint32_t subgroupSize) { + *std::find(code.begin(), code.end(), 0xf800180fu) = 0xf80008cfu; + *std::find(code.begin(), code.end(), 0x7e160f00u) = 0x7e160f01u; + RecompileRequest request{}; + request.shader = {ShaderStage::TessellationEvaluation, 0x10000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}}; + request.useCache = false; + return Recompile(request).spirv; + }; + require(reductions(domain(scan(0xffffffffu, 0x26000000u, 0x83800000u), 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: the wave64 domain UMin scan under s_orn2_saveexec_b64 vcc, exec did not read lane 31 as a subgroup UMin on 32 lanes"); + require(halves(domain(scan(0u, 0x38000000u, 0x88000000u), 64u), spv::OpGroupNonUniformBitwiseOr, 0u), "half-wave reduction: the wave64 domain OR scan did not read lanes 31 and 63 as reductions of host invocations 0-31 and 32-63 on 64 lanes"); const auto floatScan = [&scan](std::uint32_t identity, std::uint32_t vector, std::uint32_t join) { auto code = scan(identity, vector, 0u); code[23] = join; @@ -1432,6 +1676,7 @@ void verifyHalfWaveReduction() { auto overwritten = umin; overwritten.insert(overwritten.begin() + 17, {0x7ef80500u, 0x7e008514u}); expectFailure([&] { static_cast(pixel(overwritten, 32u, true)); }, lane63, "half-wave reduction: a scan of window registers read lane 63 after a v_movreld_b32 at a runtime M0 that may overwrite them"); + require(reductions(domain(divergent, 32u), spv::OpGroupNonUniformUMin) == 1u, "half-wave reduction: domain keys masked inside a divergent branch did not read lane 31 as a subgroup UMin on 32 lanes"); const char* arithmetic = "v_readlane_b32 of lane 31 of a wave64 half-wave reduction scan needs subgroup arithmetic"; expectFailure([&] { static_cast(pixel(umin, 32u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 32 lanes"); expectFailure([&] { static_cast(pixel(umin, 64u, false)); }, arithmetic, "half-wave reduction: a device without subgroup arithmetic read lane 31 on 64 lanes"); @@ -1478,7 +1723,7 @@ void verifyNarrowVertexWave() { const auto prepares = [&prepared](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { return !prepared(code, subgroupSize, userDataBase).empty(); }; - const auto reductions = [](const std::vector& spirv) { + const auto reductions = [](std::span spirv) { std::uint32_t count = 0; for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == spv::OpGroupNonUniformBitwiseOr && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; @@ -1539,8 +1784,8 @@ void verifyNarrowVertexWave() { require(first->source != second->source, "narrow vertex wave: the program prepared for 32 and for 64 lanes shares one translated source"); require(MatchesPreparedShader(narrowRequest, narrowHandle) && MatchesPreparedShader(wideRequest, wideHandle), "narrow vertex wave: a prepared program did not match its own request"); require(!MatchesPreparedShader(narrowRequest, wideHandle) && !MatchesPreparedShader(wideRequest, narrowHandle), "narrow vertex wave: a program prepared for one subgroup width matched a request for the other"); - narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv; - wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv; + narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv.Copy(); + wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv.Copy(); } require(narrow[0] == narrow[1] && wide[0] == wide[1], "narrow vertex wave: preparing for one subgroup width after the other gave another program than preparing it first"); require(narrow[0] != wide[0], "narrow vertex wave: s3 of a wave64 vertex program was the same on 32 and 64 lanes"); @@ -1569,7 +1814,7 @@ void verifySingleLaneVertexWave() { const auto compiled = [&vertex](std::span code, std::uint32_t subgroupSize, bool vertexSubgroups) { return Recompile(vertex(code, subgroupSize, vertexSubgroups)).spirv; }; - const auto usesSubgroups = [](const std::vector& spirv) { + const auto usesSubgroups = [](std::span spirv) { for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { const auto op = spirv[cursor] & 0xffffu; if (op == spv::OpCapability && spirv[cursor + 1u] >= spv::CapabilityGroupNonUniform && spirv[cursor + 1u] <= spv::CapabilityGroupNonUniformQuad) return true; @@ -1578,7 +1823,7 @@ void verifySingleLaneVertexWave() { } return false; }; - const auto hasConstant = [](const std::vector& spirv, std::uint32_t value) { + const auto hasConstant = [](std::span spirv, std::uint32_t value) { for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { if ((spirv[cursor] & 0xffffu) == spv::OpConstant && (spirv[cursor] >> 16u) == 4u && spirv[cursor + 3u] == value) return true; } @@ -1689,10 +1934,10 @@ void verifyMeshConfiguration() { RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; - hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u}, {}}; + hull.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{4u, 4u, 2u, 2u, 3u, 63u, 63u, true}, {}}; const auto hullReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(hull)); const auto* replayed = hullReplay.request.graphics && hullReplay.request.graphics->tessellation ? &*hullReplay.request.graphics->tessellation : nullptr; - require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u, "the quad tessellation configuration or its patches per group were lost in serialization"); + require(replayed != nullptr && replayed->domain == 2u && replayed->outputTopology == 3u && replayed->patchesPerGroup == 63u && replayed->primitiveGroupSize == 63u && replayed->breakAtInstance, "the quad tessellation configuration, its patches per group, its primitive group size or BREAK_WAVE_AT_EOI were lost in serialization"); auto grouped = hull; grouped.graphics->tessellation->patchesPerGroup = 62u; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { @@ -1703,6 +1948,26 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(grouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per group, which bound their relative patch index"); } + auto primitiveGrouped = hull; + primitiveGrouped.graphics->tessellation->primitiveGroupSize = 126u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + primitiveGrouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(primitiveGrouped, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); + } + auto shared = hull; + shared.graphics->tessellation->breakAtInstance = false; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + shared.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + RecompileCacheKey::Build(shared, key); + require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + } const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -1746,6 +2011,177 @@ std::string requestPrefix(std::string_view text, std::size_t bytes) { return result; } +void verifyCanonicalVersion20And21Requests() { + using namespace ShaderRecompiler; + struct Fixture { ShaderStage stage; std::uint32_t selector; bool subgroups; std::string_view text; }; + static constexpr std::array fixtures{{ + {ShaderStage::Fragment, 0u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 0u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 1u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 1u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 2u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 2u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Fragment, 3u, false, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAA="}, + {ShaderStage::Fragment, 3u, true, + "NVNQQRQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAA" + "AAF4VjQSAQEBOTAAAAE="}, + {ShaderStage::Vertex, 330245u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 330245u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + {ShaderStage::Vertex, 656650u, false, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAA="}, + {ShaderStage::Vertex, 656650u, true, + "NVNQQRQAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t" + "3gAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABCgUKAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQS" + "AQEBOTAAAAE="}, + }}; + constexpr std::array userData{0x13579bdfu, 0x2468ace0u, 0xdeadbeefu}; + constexpr std::array bytes{std::byte{0x12}, std::byte{0x34}, std::byte{0x56}, std::byte{0x78}}; + const RequestSerializer serializer; + // Original serializers at 4bb2c7f394 (20) and 075169f7b3 (21) emitted these + // twelve payloads independently; every pair differs only in its version header. + for (const auto version : {20u, 21u}) { + for (const auto& fixture : fixtures) { + auto text = std::string(fixture.text); + if (version == 21u) { + constexpr std::string_view prefix = "NVNQQRUAAAA"; + text.replace(0, prefix.size(), prefix); + } + const auto legacy = serializer.Deserialize(text); + const auto& request = legacy.request; + require(request.shader.stage == fixture.stage && request.shader.codeAddress == 0x30000u && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u, "canonical version20/21 shader identity was misread"); + require(request.context.waveSize == 64u && request.context.userDataBaseRegister == 8u && request.context.userData.size() == userData.size() && std::equal(request.context.userData.begin(), request.context.userData.end(), userData.begin()), "canonical version20/21 guest context was misread"); + require(request.context.memory.size() == 1u && request.context.memory[0].guestAddress == 0x50000u && request.context.memory[0].bytes.size() == bytes.size() && std::equal(request.context.memory[0].bytes.begin(), request.context.memory[0].bytes.end(), bytes.begin()), "canonical version20/21 trailing memory was misread"); + require(request.layout.descriptorSet == 2u && request.layout.firstBinding == 11u && request.layout.pushConstantOffsetBytes == 16u && request.layout.pushConstantSizeBytes == 64u && !request.useCache, "canonical version20/21 trailing layout or cache flag was misread"); + require(request.target.vertexSubgroups == fixture.subgroups && request.target.float64DenormPreserve && request.target.registrationModules && request.target.registrationWordLimit == 12345u && request.target.nonConstantImageOffsets && request.target.srgbDecodeFormats == 0x12345678u && request.target.narrowSubgroupClock, "canonical version20/21 target fields were misread"); + if (fixture.stage == ShaderStage::Fragment) { + require(request.context.pixel.has_value() && !request.context.vertex, "canonical version20/21 pixel presence was misread"); + const auto& pixel = *request.context.pixel; + require(pixel.rasterPrimitive == static_cast(fixture.selector) && !pixel.layerExported && !pixel.ditheredAlphaToCoverage && pixel.alphaToCoverage && pixel.stencilRefExportEnable && pixel.targetOutputMode[0] == 9u && pixel.targetExportMapping[0] == 0xe4u, "canonical version20/21 pixel fields were misread"); + } else { + require(request.context.vertex.has_value() && !request.context.pixel, "canonical version20/21 vertex presence was misread"); + const auto& vertex = *request.context.vertex; + require(vertex.paClVsOutCntl == fixture.selector && vertex.fetchAttribReg == 9u && vertex.fetchBufferReg == 13u && vertex.fetchEmbedded, "canonical version20/21 vertex fields were misread"); + } + const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); + require(RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(upgraded.request), "upgrading canonical version20/21 changed shader identity"); + require(serializer.Serialize(upgraded.request) == serializer.Serialize(request), "upgrading canonical version20/21 changed request fields"); + } + } +} void verifyPixelRequestSerialization() { using namespace ShaderRecompiler; const auto fields = [](const ShaderPixelStageInfo& value) { @@ -1754,7 +2190,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -1799,6 +2235,7 @@ void verifyPixelRequestSerialization() { for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i; pixel.alphaToCoverage = true; pixel.stencilRefExportEnable = true; + pixel.ditheredAlphaToCoverage = true; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -1823,6 +2260,10 @@ void verifyPixelRequestSerialization() { RecompileCacheKey::Build(changed, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(changed), "runtime pixel export mapping changed the static shader identity"); } + auto undithered = request; + undithered.context.pixel->ditheredAlphaToCoverage = false; + RecompileCacheKey::Build(undithered, replayKey); + require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(undithered), "dithered coverage did not change shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -1843,15 +2284,121 @@ void verifyPixelRequestSerialization() { minimal.context.pixel = ShaderPixelStageInfo{}; minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; minimal.context.pixel->layerExported = true; + minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRcAAAA=", "new requests did not use serialization version 23"); - require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip"); + require(requestPrefix(encoded, 8u) == "NVNQQRkAAAA=", "new requests did not use serialization version 25"); + require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && serializer.Deserialize(encoded).request.context.pixel->ditheredAlphaToCoverage, "the pixel primitive class or dither mode did not round-trip"); require(serializer.Deserialize(encoded).request.context.pixel->layerExported, "the pixel layer export did not round-trip"); - constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 22u; ++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 or layer export flag was accepted"); + const auto legacy16 = serializer.Deserialize( + "NVNQQRAAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAQAAAAgAAAAAAAAAAAAAAAAQ=="); + require(legacy16.request.context.pixel->alphaToCoverage && !legacy16.request.context.pixel->ditheredAlphaToCoverage, "version16 coverage acquired a dither mode"); + require(legacy16.request.context.pixel->targetOutputMode[0] == 9u && legacy16.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy16.request.target.float64DenormPreserve, "version16 export or FP64 fields were lost"); + require(legacy16.request.layout.firstBinding == 11u && legacy16.request.layout.pushConstantOffsetBytes == 16u && !legacy16.request.useCache, "version16 trailing fields were misaligned"); + const auto upgraded16 = serializer.Deserialize(serializer.Serialize(legacy16.request)); + require(RecompileCacheKey::ContextHash(legacy16.request) == RecompileCacheKey::ContextHash(upgraded16.request), "upgrading version16 changed shader identity"); + const auto legacy17 = serializer.Deserialize( + "NVNQQREAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAIAAAAAAAAAAAAAAAAEBOTAAAA=="); + require(legacy17.request.context.pixel->alphaToCoverage && legacy17.request.context.pixel->stencilRefExportEnable && !legacy17.request.context.pixel->ditheredAlphaToCoverage, "version17 coverage or stencil flags were lost"); + require(legacy17.request.target.float64DenormPreserve && legacy17.request.target.registrationModules && legacy17.request.target.registrationWordLimit == 12345u, "version17 registration or FP64 fields were lost"); + require(legacy17.request.context.pixel->targetOutputMode[0] == 9u && legacy17.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy17.request.layout.firstBinding == 11u && legacy17.request.layout.pushConstantOffsetBytes == 16u && !legacy17.request.useCache, "version17 export or trailing fields were misaligned"); + const auto upgraded17 = serializer.Deserialize(serializer.Serialize(legacy17.request)); + require(RecompileCacheKey::ContextHash(legacy17.request) == RecompileCacheKey::ContextHash(upgraded17.request), "upgrading version17 changed shader identity"); + const std::array, 2> legacy18Requests{{ + {false, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAA="}, + {true, + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAEAAAAAAAAAAAAAAAAAAAAAEBOTAAAAE="} + }}; + for (const auto& [subgroups, text] : legacy18Requests) { + const auto legacy18 = serializer.Deserialize(text); + require(legacy18.request.target.vertexSubgroups == subgroups, "version18 vertex-subgroup flag was lost"); + require(legacy18.request.context.pixel->alphaToCoverage && legacy18.request.context.pixel->stencilRefExportEnable && !legacy18.request.context.pixel->ditheredAlphaToCoverage, "version18 coverage or stencil flags were lost"); + require(legacy18.request.target.float64DenormPreserve && legacy18.request.target.registrationModules && legacy18.request.target.registrationWordLimit == 12345u, "version18 registration or FP64 fields were lost"); + require(legacy18.request.context.pixel->targetOutputMode[0] == 9u && legacy18.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy18.request.layout.firstBinding == 11u && legacy18.request.layout.pushConstantOffsetBytes == 16u && !legacy18.request.useCache, "version18 export or trailing fields were misaligned"); + const auto upgraded18 = serializer.Deserialize(serializer.Serialize(legacy18.request)); + require(RecompileCacheKey::ContextHash(legacy18.request) == RecompileCacheKey::ContextHash(upgraded18.request), "upgrading version18 changed shader identity"); + require(upgraded18.request.target.vertexSubgroups == subgroups, "upgrading version18 changed vertex-subgroup support"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) { + const std::array, std::string_view>, 8> legacy19Requests{{ + {{static_cast(0u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(0u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(1u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(1u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(2u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(2u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + {{static_cast(3u), false}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAA"}, + {{static_cast(3u), true}, + "NVNQQRMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAABAAAAAAAAAAAAAAAAAAAAABATkwAAAB"}, + }}; + for (const auto& [flags, text] : legacy19Requests) { + const auto legacy19 = serializer.Deserialize(text); + require(legacy19.request.context.pixel->rasterPrimitive == flags.first && !legacy19.request.context.pixel->ditheredAlphaToCoverage, "version19 primitive or dither mode was misread"); + require(legacy19.request.context.pixel->alphaToCoverage && legacy19.request.context.pixel->stencilRefExportEnable, "version19 coverage or stencil flags were lost"); + require(legacy19.request.target.vertexSubgroups == flags.second && legacy19.request.target.float64DenormPreserve && legacy19.request.target.registrationModules && legacy19.request.target.registrationWordLimit == 12345u, "version19 target fields were misaligned"); + require(legacy19.request.context.pixel->targetOutputMode[0] == 9u && legacy19.request.context.pixel->targetExportMapping[0] == 0xe4u && legacy19.request.layout.firstBinding == 11u && legacy19.request.layout.pushConstantOffsetBytes == 16u && !legacy19.request.useCache, "version19 export or trailing fields were misaligned"); + const auto upgraded19 = serializer.Deserialize(serializer.Serialize(legacy19.request)); + require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); + } + constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; + for (std::size_t bytes = 0; bytes < 23u; ++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 or dither flag was accepted"); + } + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -1940,11 +2487,11 @@ void verifyVertexInfoVersion19() { while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset; require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); - auto version20 = flagged; - version20[4] = 20u; - const auto replay20 = serializer.Deserialize(encodeBase64(version20)); - require(replay20.request.context.vertex.has_value() && replay20.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a version 20 vertex info lost its position export layout"); - require(serializer.Serialize(replay20.request) == marked, "a version 20 vertex info misread the fields after its position export layout"); + auto version20Vertex = flagged; + 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"); + require(serializer.Serialize(replay20Vertex.request) == marked, "a main version20 vertex request misread trailing fields"); vertex.context.vertex->paClVsOutCntl = (1u << 18u) | (1u << 21u) | 0xfu; const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); @@ -1969,11 +2516,13 @@ void verifyVertexInfoVersion19() { std::size_t flagOffset = 0; while (flagOffset < legacyPixel.size() && legacyPixel[flagOffset] == exportedBytes[flagOffset]) ++flagOffset; require(flagOffset < legacyPixel.size(), "the pixel layer export flag is not serialized"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset)); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 2u)); for (const auto version : {19u, 20u, 21u, 22u}) { - legacyPixel[4] = static_cast(version); - const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel)); - require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported, "a main version 19 to 22 request lost its pixel primitive class or read a layer export flag"); + auto versionPixel = legacyPixel; + versionPixel[4] = static_cast(version); + if (version < 22u) versionPixel.pop_back(); + const auto replayPixel = serializer.Deserialize(encodeBase64(versionPixel)); + require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage, "a main version 19 to 22 request lost its pixel primitive class or read a layer export or dither flag"); require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class"); } } @@ -2062,7 +2611,7 @@ void verifyLegacyPixelRequests() { require(pixel.targetExportMapping == std::array{}, "legacy pixel mapping no longer defaults to zero"); require(pixel.targetExportPacking == std::array{}, "a legacy request packed a color export"); require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); - require(!pixel.alphaToCoverage, "a legacy request enabled alpha-to-coverage"); + require(!pixel.alphaToCoverage && !pixel.ditheredAlphaToCoverage, "a legacy request enabled alpha-to-coverage"); require(!pixel.stencilRefExportEnable, "a legacy request exported a stencil reference"); require(pixel.rasterPrimitive == RasterPrimitive::Triangle, "a legacy request drew something other than triangles"); require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread"); @@ -2287,7 +2836,7 @@ void verifyComputedTexelOffsets() { }; const auto computed = program(0x100u, 0u); const auto constant = program(0xffu, 0x3fu | (1u << 8u)); - const auto recompile = [&](const std::vector& code, bool offsets) { + const auto recompile = [&](std::span code, bool offsets) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x30000u, code, 0, {}}; request.target = BufferTarget(); @@ -2307,7 +2856,7 @@ void verifyComputedTexelOffsets() { request.context.memory = memory.Regions(); return Recompile(request).spirv; }; - const auto sampleOperands = [](const std::vector& words) { + const auto sampleOperands = [](std::span words) { std::uint32_t mask = 0; bool gatherExtended = false; for (std::size_t cursor = 5; cursor < words.size();) { @@ -2392,7 +2941,7 @@ void verifyInt64AtomicCapabilities() { 0u, 0u, 0u, 0u}; const std::vector bufferAtomic{0xe1705000u, 0x80012803u, 0xbf810000u}; const std::vector imageAtomic{0xf03c0308u, 0x00020a08u, 0xbf810000u}; - const auto compile = [&](const std::vector& code, spv::Capability added) { + const auto compile = [&](std::span code, spv::Capability added) { const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, static_cast(added)}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x58000u, code, 0, {}}; @@ -2411,7 +2960,7 @@ void verifyInt64AtomicCapabilities() { request.useCache = false; return Recompile(request).spirv; }; - const auto declares = [](const std::vector& words, spv::Capability capability) { + const auto declares = [](std::span words, spv::Capability capability) { for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; require(count != 0 && count <= words.size() - cursor, "64-bit atomics: truncated SPIR-V instruction"); @@ -2468,7 +3017,7 @@ void verifySpecializedBufferAtomicBindings() { require(tables == 0, "specialized buffer atomic: a buffer_atomic_inc through a V# the program specializes binds the address tables"); require(faults == 1 && result.bdaAbiVersion == BdaAbi::Version, "specialized buffer atomic: the program has no fault buffer for its unaligned atomic"); require(atomicElements == 1, "specialized buffer atomic: the V# element is not reported as atomic"); - const std::vector& words = result.spirv; + const auto words = result.spirv.Words(); std::uint32_t faultFunction = 0; std::size_t faultCalls = 0; bool physical = false; @@ -2652,7 +3201,7 @@ void verifyUnnormalizedSamplers() { }; const std::array capabilities{1u, static_cast(spv::CapabilityImageGatherExtended), static_cast(spv::CapabilityMinLod)}; std::uint64_t nextAddress = 0x40000u; - const auto recompile = [&](const std::vector& code, const std::array& data, std::uint64_t address = 0u, bool offsets = false) { + const auto recompile = [&](std::span code, const std::array& data, std::uint64_t address = 0u, bool offsets = false) { RecompileRequest request{}; request.shader = {ShaderStage::Compute, address != 0u ? address : (nextAddress += 0x1000u), code, 0, {}}; request.context.waveSize = 32; @@ -2680,7 +3229,7 @@ void verifyUnnormalizedSamplers() { const auto proven = [&](const RecompileResult& result, bool expected) { return flags(result, DescriptorRole::GuestSamplers) == std::vector{expected} && flags(result, DescriptorRole::GuestImages) == std::vector{expected}; }; - const auto auditSpirv = [](const std::vector& words) { + const auto auditSpirv = [](std::span words) { std::size_t samples = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -2702,7 +3251,7 @@ void verifyUnnormalizedSamplers() { auditSpirv(accepted.spirv.Words()); const auto plain = recompile(lz, userData(normalized), 0x40000u); require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged"); - require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant"); + require(plain.variantId == accepted.variantId && plain.spirv == accepted.spirv, "unnormalized samplers: the unnormalized S# compiled another variant"); for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u, 0xf0880f08u}) { const auto result = recompile(program(mimg), userData(unnormalized)); @@ -2736,7 +3285,7 @@ void verifyUnnormalizedSamplers() { code.insert(code.end(), {0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u}); return code; }; - const auto foldedSamples = [](const std::vector& words) { + const auto foldedSamples = [](std::span words) { std::vector exact; std::vector sums; std::vector coordinates; @@ -2751,7 +3300,7 @@ void verifyUnnormalizedSamplers() { } return !coordinates.empty() && std::ranges::all_of(coordinates, [&](std::uint32_t id) { return std::ranges::count(sums, id) != 0 && std::ranges::count(exact, id) != 0; }); }; - const auto sampleMasks = [](const std::vector& words) { + const auto sampleMasks = [](std::span words) { std::vector masks; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -2811,7 +3360,7 @@ void verifyUnnormalizedSamplers() { pixel.target.subgroupSize = 64; pixel.layout.pushConstantSizeBytes = 128; pixel.useCache = false; - const auto implicitSamples = [](const std::vector& words) { + const auto implicitSamples = [](std::span words) { std::size_t samples = 0; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; @@ -3008,10 +3557,10 @@ void verifyBdaReadFallbackFunctions() { alignas(256) static std::array output{}; const auto outputAddress = static_cast(reinterpret_cast(output.data())); const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; - const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent) { + const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent, std::uint32_t stride) { std::vector code{0x7e020200u, 0x7e040201u}; for (std::uint32_t load = 0; load < loads; ++load) { - code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (4u * load + 4u)); + code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (stride * load + 4u)); code.push_back(((3u + load) << 24u) | 0x007d0001u); } code.push_back(0xbf8c3f70u); @@ -3037,8 +3586,9 @@ void verifyBdaReadFallbackFunctions() { }; for (const bool barrier : {false, true}) { for (const bool coherent : {false, true}) { - const auto one = compile(1u, barrier, coherent); - const auto five = compile(5u, barrier, coherent); + const auto one = compile(1u, barrier, coherent, 32u); + const auto five = compile(5u, barrier, coherent, 32u); + const auto adjacent = compile(5u, barrier, coherent, 4u); std::map names; std::map definitions; const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); @@ -3054,6 +3604,9 @@ void verifyBdaReadFallbackFunctions() { std::size_t loaderLoads = 0; std::size_t loaderCalls = 0; std::array readerLoads{}; + std::size_t lookupFaults = 0; + std::size_t lookupVariables = 0; + std::size_t readerLookups = 0; for (std::size_t cursor = 5; cursor < five.size();) { const auto length = five[cursor] >> 16u; require(length != 0 && length <= five.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); @@ -3078,6 +3631,9 @@ void verifyBdaReadFallbackFunctions() { if ((op == spv::OpLoad || op == spv::OpAtomicLoad) && function == loader) ++loaderLoads; if (op == spv::OpFunctionCall && function == loader) ++loaderCalls; if (op == spv::OpLoad && function == reader) ++readerLoads[length > 4u && (five[cursor + 4] & spv::MemoryAccessVolatileMask) != 0u]; + if (op == spv::OpFunctionCall && function == "get_bda_pointer" && names[five[cursor + 3]] == "record_bda_fault") ++lookupFaults; + if (op == spv::OpVariable && function == "get_bda_pointer") ++lookupVariables; + if (op == spv::OpFunctionCall && function == reader && names[five[cursor + 3]] == "get_bda_pointer") ++readerLookups; cursor += length; } require(definitions["record_bda_fault"] == 1u && definitions[reader] == 1u && definitions[span] == 1u, "BDA read functions: the fault, byte read and span read functions are not defined"); @@ -3086,10 +3642,26 @@ void verifyBdaReadFallbackFunctions() { for (const auto& [name, count] : definitions) require(count == 1u, "BDA read functions: a function is defined twice"); require(compareExchanges == 1u, "BDA read functions: a fault is recorded outside record_bda_fault"); require(mainLookups == 0u, "BDA read functions: a read site looks up its bytes inline"); - require(readerLoads[coherent] == 4u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(readerLoads[coherent] == 1u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence"); + require(lookupFaults <= 4u, "BDA read functions: the page-table lookup records its faults from more than 4 call sites"); + require(lookupVariables == 0u, "BDA read functions: the page-table lookup keeps its search bounds in function variables"); + require(readerLookups == 1u, "BDA read functions: the byte read does not look up its bytes once, in a loop"); require(mainProbes == 0u, "BDA read functions: a read site probes or reads bytes outside its span read function"); require(spanCalls == 5u, "BDA read functions: a read site does not call its span read function once"); require(five.size() - one.size() < 4u * 150u, "BDA read functions: a read site takes 150 SPIR-V words or more"); + std::size_t adjacentSpanCalls = 0; + names.clear(); + function.clear(); + for (std::size_t cursor = 5; cursor < adjacent.size();) { + const auto length = adjacent[cursor] >> 16u; + require(length != 0 && length <= adjacent.size() - cursor, "BDA read functions: truncated SPIR-V instruction"); + const auto op = adjacent[cursor] & 0xffffu; + if (op == spv::OpName) names[adjacent[cursor + 1]] = reinterpret_cast(&adjacent[cursor + 2]); + if (op == spv::OpFunction) function = names[adjacent[cursor + 2]]; + if (op == spv::OpFunctionCall && function == "main" && names[adjacent[cursor + 3]] == span) ++adjacentSpanCalls; + cursor += length; + } + require(adjacentSpanCalls == 2u, "BDA read functions: five adjacent dwords of one address do not share two span reads"); } } } @@ -3204,6 +3776,13 @@ void verifyFunctionLdsBound() { }); require(unbounded == FunctionLdsDwordLimit && unboundedRequirements.functionLdsDwords == FunctionLdsDwordLimit, "function LDS: an address without a bound must keep the full array"); + const auto [counter, counterRequirements] = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { + auto& m0 = emit(IrOpcode::GetUserData, IrType::U32, {&constant(0u)}); + emit(IrOpcode::DataAppend, IrType::U32, {&m0, &active}, 2u); + emit(IrOpcode::DataConsume, IrType::U32, {&m0, &active}, 1u); + }); + require(counter == 192u && counterRequirements.functionLdsDwords == 192u, "function LDS: an LDS append or consume must size the array by its offset, not by M0"); + const auto wrapping = build([](IrProgram&, IrBlock&, auto& emit, auto& constant, IrValue& active) { emit(IrOpcode::LoadSharedU32, IrType::U32, {&constant(0x20u), &active}, 3u); }).first; @@ -3234,6 +3813,81 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } +void verifyFunctionLdsUpdates() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; + const auto compile = [](ShaderStage stage, std::uint32_t waveSize, std::span body) { + std::vector code{0x7e000280u, 0x7e020292u, 0xbefc03ffu, 0x0000ffffu}; + code.insert(code.end(), body.begin(), body.end()); + code.insert(code.end(), {0xbf8cc07fu, 0xd8d80000u, 0x02000000u, 0xbf8cc07fu, stage == ShaderStage::Fragment ? 0xf800180fu : 0xf80008cfu, 0x02020202u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {stage, 0x40000u, code, 0, {}}; + request.context.waveSize = waveSize; + if (stage == ShaderStage::Fragment) { + ShaderPixelStageInfo info{}; + info.wave32 = waveSize == 32u; + info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter); + info.hasPerspectiveCenterVgpr = true; + info.targetOutputMode[0] = 9u; + info.targetExportMapping.fill(0xe4u); + request.context.pixel = info; + } else { + request.context.vertex = ShaderVertexStageInfo{}; + } + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto violations = [](std::span words) { + std::map storage; + std::map constants; + std::map types; + 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::OpTypePointer) storage[words[at + 1]] = words[at + 2]; + if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + if (op == spv::OpVariable || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) types[words[at + 2]] = words[at + 1]; + } + const auto workgroupScope = [&](std::uint32_t id) { return constants.count(id) != 0 && constants.at(id) == spv::ScopeWorkgroup; }; + std::size_t found = 0; + 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::OpMemoryBarrier && workgroupScope(words[at + 1])) ++found; + if ((op < spv::OpAtomicLoad || op > spv::OpAtomicXor) && op != spv::OpAtomicFMinEXT && op != spv::OpAtomicFMaxEXT && op != spv::OpAtomicFAddEXT) continue; + const auto pointer = words[at + (op == spv::OpAtomicStore ? 1u : 3u)]; + const auto type = types.find(pointer); + if (type != types.end() && storage.count(type->second) != 0 && storage.at(type->second) == spv::StorageClassFunction) ++found; + if (workgroupScope(words[at + (op == spv::OpAtomicStore ? 2u : 4u)])) ++found; + } + return found; + }; + using Program = std::pair>; + static constexpr std::array programs{{ + {"ds_write_b8", {0xd8780000u, 0x00000100u}}, + {"ds_write_b16", {0xd87c0000u, 0x00000100u}}, + {"ds_write_b8_d16_hi", {0xda800000u, 0x00000100u}}, + {"ds_write_b16_d16_hi", {0xda840000u, 0x00000100u}}, + {"ds_add_u32", {0xd8000000u, 0x00000100u}}, + {"ds_inc_rtn_u32", {0xd88c0000u, 0x03000100u}}, + {"ds_mskor_b32", {0xd8300000u, 0x00010100u}}, + {"ds_append", {0xd8f80000u, 0x03000000u}}, + }}; + for (const auto& [name, body] : programs) { + for (const auto stage : {ShaderStage::Fragment, ShaderStage::Vertex}) { + for (const auto waveSize : {64u, 32u}) { + const auto what = std::string("function LDS: ") + name + (stage == ShaderStage::Fragment ? " in a pixel" : " in a vertex") + " wave" + std::to_string(waveSize) + " program"; + const auto result = compile(stage, waveSize, body); + require(!result.spirv.empty(), (what + " did not compile").c_str()); + require(violations(result.spirv.Words()) == 0, (what + " uses an atomic on a Function pointer or a Workgroup memory scope").c_str()); + } + } + } +} + void verifySrtReadPatching() { using namespace ShaderRecompiler; IrProgram program; @@ -3522,6 +4176,28 @@ void verifyCubeDepthCompareMode() { require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); } +void verifyVolumeArrayLoadMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x02466600u, 0x01400000u, 0u, 0xa0500204u, 1u, 0x00700000u, 0u, 0u}, 8u}; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2DArray; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + 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"); + ImageResource store{}; + store.resourceClass = ImageResourceClass::Storage; + store.dimension = RdnaImageDimension::Dim2DArray; + store.written = true; + store.fmaskCompatible = false; + const auto storeModes = ResourceMaterializer::RuntimeImageModes(store); + require(storeModes[ResourceMaterializer::RuntimeImageMode(store, descriptor, storeModes)].dimension == RdnaImageDimension::Dim3D, "2D-array image store on a 3D T# did not select a volume runtime mode"); + store.atomic = true; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -3548,6 +4224,11 @@ int main(int argc, char** argv) { std::cout << "Prepared frontend reuse preserves full and partial-thread programs\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--layout-pair") { + verifyLayoutPair(); + std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--bindless") { verifyBindlessTable(); std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; @@ -3562,6 +4243,7 @@ int main(int argc, char** argv) { verifyRegisterSources(); verifyEvaluatedValues(); verifyFrontendPair(); + verifyLayoutPair(); verifySignedSrtComparison(); verifyOutOfRangeConstantReads(); verifySignedMinimum(); @@ -3569,6 +4251,7 @@ int main(int argc, char** argv) { verifyGuardedDescriptorSources(); verifyBindlessTable(); verifyDescriptorPhis(); + verifyUniformTableImage(); verifyLaneSelectedDescriptors(); verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); @@ -3577,6 +4260,7 @@ int main(int argc, char** argv) { verifySingleLaneVertexWave(); verifyMeshConfiguration(); verifyPixelInputs(); + verifyCanonicalVersion20And21Requests(); verifyPixelRequestSerialization(); verifyVertexInfoVersion19(); verifyPositionExportComponents(); @@ -3595,9 +4279,11 @@ int main(int argc, char** argv) { verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); verifyFunctionLdsBound(); + verifyFunctionLdsUpdates(); verifySrtReadPatching(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); + verifyVolumeArrayLoadMode(); verifySrgbDepthCompareModes(); verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp index dda775da27..3add236bdb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp @@ -41,7 +41,7 @@ std::vector recompile(std::span code) { request.useCache = false; auto result = Recompile(request); require(!result.spirv.empty(), "the shader did not recompile"); - return std::move(result.spirv); + return result.spirv.Copy(); } void requireDistinctTargets(const char* name, std::span code) { diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index cf6e8a94fe..d067e97200 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -2,6 +2,7 @@ #include "SpirvBackend/SpirvSpecialization.hpp" #include #include +#include #include #include #include @@ -285,8 +286,21 @@ void testFoldingLeavesNoDeadBlocksOrConstants() { checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids"); } +void testIdsBeyondTheBoundAreRejected() { + auto words = foldedSwitchModule(13u, {1u, 1u, 1u}); + words[3] = 12u; + bool rejected = false; + try { + static_cast(ShaderRecompiler::SpecializeSpirv(words)); + } catch (const std::exception&) { + rejected = true; + } + check(rejected, "a module whose ids reach its bound was specialized"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); + testIdsBeyondTheBoundAreRejected(); testStructExtractsAreNotShuffled(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index ae99defb93..b63b6b04ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -44,7 +44,7 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { if (message != nullptr) diagnostics += std::string(message) + '\n'; }); - const bool valid = validator.Validate(words); + const bool valid = validator.Validate(words.data(), words.size()); Check(valid, std::string(name) + ": emitted invalid SPIR-V:\n" + diagnostics); #endif } diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index e8b1bbee05..ca8b02d465 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -174,6 +176,92 @@ void requireStructuredLoop(const std::vector>& succes requireOriginalBranches(graph, successors, name); } +const BasicBlock& originalCopy(const ControlFlowGraph& graph, std::uint32_t original) { + const auto copy = std::find_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return block.instructionBegin == original * 2 && block.instructionEnd == original * 2 + 2; }); + if (copy == graph.blocks.end()) throw std::runtime_error("original block " + std::to_string(original) + " is missing"); + return *copy; +} + +std::uint32_t guestBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes, const char* name) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) { + if (terminator.gotoValue >= 0) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " sets a route variable after guest instructions"); + return blockId; + } + if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0; + if (terminator.kind == TerminatorKind::Branch) { + blockId = terminator.trueBlock; + } else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable) { + const auto route = routes.find(terminator.gotoVariable); + if (route == routes.end()) throw std::runtime_error(std::string(name) + ": block " + std::to_string(blockId) + " reads route variable " + std::to_string(terminator.gotoVariable) + " before any path sets it"); + blockId = route->second ? terminator.trueBlock : terminator.falseBlock; + } else { + return blockId; + } + } + throw std::runtime_error(std::string(name) + ": a cycle of empty blocks"); +} + +void requireSameExecutions(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + if (structured.blocks.empty() || structured.entryBlock != 0 || structured.blocks.front().id != 0) { + throw std::runtime_error(std::string(name) + ": the structured graph does not start at block 0"); + } + using State = std::tuple; + std::vector pending{{0u, original.entryBlock, {}}}; + std::set visited; + while (!pending.empty()) { + auto [structuredId, originalId, routes] = std::move(pending.back()); + pending.pop_back(); + const auto copyId = guestBlock(structured, structuredId, routes, name); + if (!visited.emplace(copyId, originalId, routes).second) continue; + const auto& copy = structured.FindBlock(copyId); + const auto& source = original.FindBlock(originalId); + if (copy.instructionBegin != source.instructionBegin || copy.instructionEnd != source.instructionEnd || copy.terminator.kind != source.terminator.kind || copy.terminator.condition != source.terminator.condition) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(copyId) + " does not run like original block " + std::to_string(originalId)); + } + if (source.terminator.kind == TerminatorKind::Branch || source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.trueBlock, source.terminator.trueBlock, routes); + if (source.terminator.kind == TerminatorKind::ConditionalBranch) pending.emplace_back(copy.terminator.falseBlock, source.terminator.falseBlock, routes); + } +} + +void requireUnconditionalEntry(const ControlFlowGraph& original, const ControlFlowGraph& structured, const char* name) { + const auto& entry = original.FindBlock(original.entryBlock); + auto blockId = structured.blocks.front().id; + for (std::size_t step = 0; step <= structured.blocks.size(); ++step) { + const auto& block = structured.FindBlock(blockId); + if (block.instructionBegin == entry.instructionBegin && block.instructionEnd == entry.instructionEnd) return; + if (block.instructionBegin != block.instructionEnd || block.terminator.kind != TerminatorKind::Branch) break; + blockId = block.terminator.trueBlock; + } + throw std::runtime_error(std::string(name) + ": the entry block does not run unconditionally first"); +} + +void requireSameStructuredExecutions(const std::vector>& successors, const char* name, const std::map& words = {}) { + auto original = makeGraph(successors); + for (const auto& [blockId, count] : words) original.blocks[blockId].estimatedSpirvWords = count; + auto graph = original; + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, name); + requireSameExecutions(original, graph, name); + requireUnconditionalEntry(original, graph, name); +} + +void requireGrowthBudgetRefusal(ControlFlowGraph graph, const char* name) { + const std::string growthBudget = "CFG shared merge splitting exceeded growth budget"; + try { + Structurizer{}.Structurize(graph); + } catch (const std::runtime_error& error) { + if (std::string(error.what()).starts_with(growthBudget)) return; + throw std::runtime_error(std::string(name) + ": refused with \"" + error.what() + "\" instead of the shared merge splitting growth budget"); + } + throw std::runtime_error(std::string(name) + ": was structurized instead of refused at the shared merge splitting growth budget"); +} + std::vector> originalPostDominators(const ControlFlowGraph& graph) { std::vector all; for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); @@ -290,8 +378,8 @@ int main() { auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}); Structurizer{}.Structurize(nested); requireExactPostDominators(nested, "nested selections"); - if (nested.FindBlock(2).terminator.mergeBlock == nested.FindBlock(3).terminator.mergeBlock) { - std::fprintf(stderr, "the nested selections share merge block %u\n", nested.FindBlock(2).terminator.mergeBlock); + if (originalCopy(nested, 2).terminator.mergeBlock == originalCopy(nested, 3).terminator.mergeBlock) { + std::fprintf(stderr, "the nested selections share merge block %u\n", originalCopy(nested, 2).terminator.mergeBlock); return 1; } requireStructuredBranches(nested, "nested selections"); @@ -355,6 +443,22 @@ int main() { } Structurizer{}.Structurize(sharedDiscard); requireStructuredBranches(sharedDiscard, "early discards that share one ending block"); + requireSameStructuredExecutions({{1, 2}, {3, 0}, {}, {2}}, "an entry loop with two exits whose merges collide"); + requireSameStructuredExecutions({{2}, {}, {4, 1}, {1}, {2, 3}}, "a selection region that wraps around to its dominator"); + requireSameStructuredExecutions({{1}, {2, 3}, {0, 4}, {}, {3}}, "an entry loop whose merge splitting runs out of budget"); + requireSameStructuredExecutions({{2, 5}, {4, 1}, {1, 3}, {}, {}, {1}}, "a self-loop entered from both arms of the entry selection"); + requireSameStructuredExecutions({{1, 2}, {2, 3}, {1, 3}, {}}, "an irreducible cycle entered at both blocks"); + requireSameStructuredExecutions({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches compared run by run"); + requireSameStructuredExecutions({{1, 2}, {2, 0}, {1}}, "a cycle entered at both blocks inside a loop that never ends"); + requireSameStructuredExecutions({{1, 3}, {2}, {1}, {}}, "a branch that returns or enters a loop that never ends"); + requireSameStructuredExecutions({{1}, {1}}, "a block that branches to itself forever"); + requireSameStructuredExecutions({{1, 13}, {2, 8}, {3, 6}, {4, 0}, {5, 4}, {6}, {}, {}, {5, 9}, {10, 0}, {11}, {11, 12}, {}, {14}, {15}, {16, 17}, {4}, {18, 19}, {7, 0}, {}}, "merge splitting that adds one forwarder per round until its budget runs out"); + const std::vector> junctionLoop{{1, 2}, {}, {3, 10}, {4, 8}, {6, 0}, {}, {7, 8}, {0}, {5, 9}, {7}, {9, 11}, {11}}; + requireSameStructuredExecutions(junctionLoop, "a loop whose latch is too large to clone, so merge splitting keeps adding junctions", {{7u, 2000u}}); + auto unsupportedExit = makeGraph(junctionLoop); + unsupportedExit.blocks[7].estimatedSpirvWords = 2000u; + unsupportedExit.blocks[5].terminator.kind = TerminatorKind::Unsupported; + requireGrowthBudgetRefusal(std::move(unsupportedExit), "a junction loop whose unsupported exit keeps the reach-flag fallback out"); requireOriginalDominanceSets(makeGraph(diamonds), "raw diamond chain"); requireOriginalDominanceSets(makeGraph({{1}, {2, 3}, {1}, {}}), "a loop with an exit"); requireOriginalDominanceSets(makeGraph({{1}, {2}, {1}}), "a non-exiting SCC"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 21a2806af9..721edbaa26 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -658,6 +658,39 @@ void testProgramWorkgroupUse() { check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry"); } +void testVertexTailSnapshots() { + using namespace AgcDriver::DriverDetail; + alignas(256) static std::array prolog{0xbe802008u, 0xbf9f0000u}; + alignas(256) static std::array continuation{0xbf800000u, 0xbf810000u}; + const auto address = reinterpret_cast(prolog.data()); + const auto next = reinterpret_cast(continuation.data()); + const std::array data{static_cast(next), static_cast(next >> 32u)}; + const auto source = ReadRawShader(address); + check(source->userDataTailRegister == 8u, "raw tail metadata is missing"); + const auto target = ReadUserDataTailContinuation(source->code, 8u, data); + check(target && target->codeAddress == next, "tail did not read its user-data continuation"); + DrawDecode decode{}; + decode.programs.resize(1); + decode.programs[0].snapshot = source; + decode.programs[0].tailContinuation = target; + check(DecodeProgramsCurrent(decode, {}), "unchanged tail snapshot became stale"); + continuation[0] = 0xbf8a0000u; + check(!DecodeProgramsCurrent(decode, {}), "rewriting the continuation did not invalidate the draw cache"); + AgcDriver::QueueState queue{}; + queue.context[0x2d5] = 0x2000u; + queue.shader = {{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, + {0x8b, 4u}, {0x8c, data[0]}, {0x8d, data[1]}}; + check(VertexWorkgroupRequired(queue, {}), "a barrier in the continuation was hidden from workgroup routing"); + queue.context[0x2d5] |= 0x02000000u; + check(!VertexWorkgroupRequired(queue, {}), "a passthrough program was rerouted to a workgroup"); + const std::array nullAddress{}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, nullAddress)); }).find("address is null") != std::string::npos, "null tail address was accepted"); + const std::array unaligned{data[0] + 1u, data[1]}; + check(expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unaligned)); }).find("dword aligned") != std::string::npos, "unaligned tail address was accepted"); + const std::array unmapped{0x1000u, 0u}; + check(!expectFailure([&] { static_cast(ReadUserDataTailContinuation(source->code, 8u, unmapped)); }).empty(), "unmapped tail address was accepted"); +} + void testRegisteredVertexWorkgroupRouting() { using namespace AgcDriver::DriverDetail; ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}}; @@ -781,6 +814,7 @@ int main(int argc, char** argv) { #endif testProgramSnapshots(); testProgramWorkgroupUse(); + testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); testEvents(); testValidation(); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 33d1a9f457..16f6aa1e66 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -1,6 +1,7 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include @@ -48,6 +49,20 @@ void srgbDecodeTests() { Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views"); } +void srgbTwinTests() { + constexpr std::uint32_t srgb8 = 1u; + constexpr std::uint32_t srgb8_8 = 2u; + Context context{}; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB && SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_R8G8_SRGB, "a device that samples R8_SRGB and R8G8_SRGB must give R8 and R8G8 their sRGB twins"); + context.srgbDecodeFormats = srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_8_SRGB in the shader must not give R8G8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_R8_SRGB, "R8 must keep its sRGB twin when only 8_8_SRGB is decoded in the shader"); + context.srgbDecodeFormats = srgb8 | srgb8_8; + Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); + Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); +} + template void reject(TAction action, std::string_view reason) { try { @@ -146,6 +161,7 @@ void packedFloatDccClearTests() { void RunTextureFormatTests() { srgbDecodeTests(); + srgbTwinTests(); packedChannelTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM"); @@ -153,6 +169,15 @@ void RunTextureFormatTests() { Require(!IsBlockCompressed(1), "format 1 must not be block compressed"); Require(BlockWidth(1) == 1u && BlockHeight(1) == 1u, "format 1 must have a one-texel block"); + Require(ResolveTextureFormat(50) == VK_FORMAT_A2B10G10R10_UNORM_PACK32, "format 50 must resolve to A2B10G10R10_UNORM"); + Require(ResolveTextureFormat(51) == VK_FORMAT_A2B10G10R10_SNORM_PACK32, "format 51 must resolve to A2B10G10R10_SNORM"); + Require(ResolveTextureFormat(54) == VK_FORMAT_A2B10G10R10_UINT_PACK32, "format 54 must resolve to A2B10G10R10_UINT"); + Require(ResolveTextureFormat(55) == VK_FORMAT_A2B10G10R10_SINT_PACK32, "format 55 must resolve to A2B10G10R10_SINT"); + for (const std::uint32_t format : {50u, 51u, 54u, 55u}) Require(BytesPerElement(format) == 4u && !IsBlockCompressed(format), "packed 10-bit format " + std::to_string(format) + " must be four bytes and not block compressed"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SNorm) == ShaderRecompiler::IrTextureNumericClass::Float, "format 51 must sample as float"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2SInt) == ShaderRecompiler::IrTextureNumericClass::Sint, "format 55 must sample as signed integer"); + Require(ShaderRecompiler::SampledTextureNumericClass(ShaderRecompiler::IrBufferFormat::Format10_10_10_2UInt) == ShaderRecompiler::IrTextureNumericClass::Uint, "format 54 must sample as unsigned integer"); + Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM"); Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide"); diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp index 27a7785a5a..1b8f715065 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp @@ -16,7 +16,7 @@ struct Module { std::uint32_t version = 0; }; -Module inspect(const std::vector& words) { +Module inspect(std::span words) { Module module; if (words.size() > 1) module.version = words[1]; std::vector layerIds; @@ -32,7 +32,7 @@ Module inspect(const std::vector& words) { return module; } -bool has(const std::vector& values, std::uint32_t value) { +bool has(std::span values, std::uint32_t value) { for (const auto entry : values) { if (entry == value) return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp index f362f67d6a..52027341ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -61,6 +61,7 @@ int main() { {{0xd6000005u, 0x02020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, {{0xd6000105u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, {{0xd6020005u, 0x22000200u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + {{0xd7424005u, 0x00020201u}, "modifiers are not implemented", "v_interp_p1ll_f16 with the high-half destination selector"}, }; int failures = 0; for (const auto& test : accepted) { @@ -104,5 +105,21 @@ int main() { ++failures; } } + for (const std::uint32_t selector : {0x0u, 0x4000u}) { + const std::vector code{0xd75a0005u | selector, 0x041e0201u, 0xbf810000u}; + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto& instruction = decoded.instructions.at(0); + const std::uint32_t half = selector != 0u ? 5u : 6u; + if (instruction.op != RdnaOpcode::VInterpP2F16 || instruction.destination.reg != Destination || instruction.destination.sdwaSel != half || + instruction.sourceCount != 4u || instruction.source0.reg != 1u || instruction.source1.value != 1u || instruction.source3.reg != 7u) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s decoded to the wrong opcode or operands\n", selector != 0u ? " op_sel:[0,0,0,1]" : ""); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "v_interp_p2_f16 v5, v1, attr1.x, v7%s was refused: %s\n", selector != 0u ? " op_sel:[0,0,0,1]" : "", error.what()); + ++failures; + } + } return failures == 0 ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index f839edd79d..cec733ba76 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -275,6 +275,76 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } +void CheckUntrackedHint() { + constexpr std::size_t bytes = 4 * Block; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory), bytes, PAGE_READWRITE, 16384); + struct Cleanup { + void* memory; + ~Cleanup() { + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + } + } cleanup{const_cast(memory)}; + const auto base = reinterpret_cast(memory); + const auto hinted = base + Block; + constexpr std::size_t hintedBytes = 2 * Block; + for (std::size_t offset = 0; offset < bytes; offset += 4096) memory[offset] = 0x11; + BumpCollectEpoch(); + Require(CollectWrites(base, bytes) != 0 && UntrackedHint(base, bytes) == 0, "watched memory has an untracked hint"); + Unwatch(hinted, hintedBytes); + const auto stamp = NewestStamp(hinted, hintedBytes); + const auto first = UntrackedHint(hinted, hintedBytes); + Require(first != 0, "memory that left the watch has no untracked hint"); + Require(UntrackedHint(base, bytes) == 0 && UntrackedHint(hinted - 4096, 8192) == 0, "a range with watched blocks has an untracked hint"); + std::array host{}; + Require(UntrackedHint(reinterpret_cast(host.data()), host.size()) == 0, "memory outside the arena has an untracked hint"); + Require(!Watched(hinted, hintedBytes) && CollectWrites(hinted, hintedBytes) == 0 && !UnchangedSince(hinted, hintedBytes, TrackerGeneration()), "a hinted range reads as tracked"); + std::array generations{first, first}; + std::array changed{}; + Require(!ChangedBlocks(hinted, hintedBytes, generations, changed), "a hinted range uses block stamps"); + for (unsigned epoch = 0; epoch < 8; ++epoch) { + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint moved without a store"); + } + memory[Block + 3 * 16384 + 8] ^= 1; + Require(UntrackedHint(hinted, hintedBytes) == first, "an untracked hint was walked twice in one epoch"); + BumpCollectEpoch(); + const auto stored = UntrackedHint(hinted, hintedBytes); + Require(stored > first, "a CPU store into a hinted range did not move its hint"); + Require(UntrackedHint(hinted + Block, Block) == first, "a CPU store moved the hint of another block"); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == stored, "an untracked hint moved again without a store"); + memory[Block + 8] = memory[Block + 8]; + BumpCollectEpoch(); + const auto rewritten = UntrackedHint(hinted, hintedBytes); + Require(rewritten > stored, "a store of the bytes already there did not move the hint"); + std::thread([&] { memory[2 * Block + 5 * 4096] ^= 1; }).join(); + BumpCollectEpoch(); + const auto threaded = UntrackedHint(hinted, hintedBytes); + Require(threaded > rewritten, "a store from another thread did not move the hint"); + Require(UntrackedHint(hinted + Block, Block) == threaded && UntrackedHint(hinted, Block) == rewritten, "a walk stamped a store into the wrong block"); + Require(NewestStamp(hinted, hintedBytes) == stamp, "untracked hint walks advanced the block stamps"); + Unwatch(hinted, hintedBytes); + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == threaded, "unwatching a hinted range again moved its hint"); + Unwatch(base, Block); + const auto own = UntrackedHint(base, Block); + Require(own > threaded && UntrackedHint(hinted, hintedBytes) == threaded, "a range leaving the watch did not start a hint of its own"); + GuestArena::GuestArenaReset_nid_postfix(const_cast(memory) + Block, hintedBytes); + GuestArena::GuestArenaCommit_nid_postfix(const_cast(memory) + Block, hintedBytes, PAGE_READWRITE, 16384); + BumpCollectEpoch(); + const auto fresh = CollectWrites(hinted, hintedBytes); + Require(Watched(hinted, hintedBytes) && fresh != 0 && UnchangedSince(hinted, hintedBytes, fresh), "fresh backing of a hinted range is not watched"); + memory[Block + 8] = 0x5a; + BumpCollectEpoch(); + Require(UntrackedHint(hinted, hintedBytes) == 0, "watched fresh backing has an untracked hint"); + CollectWritesUncached(hinted, hintedBytes); + Require(!UnchangedSince(hinted, Block, fresh), "an untracked hint walk took a CPU store from watched memory"); + Unwatch(hinted, hintedBytes); + Require(UntrackedHint(hinted, hintedBytes) > own, "fresh backing leaving the watch did not start a new hint"); +} + std::uint64_t ChildHostHole() { std::uintptr_t arena = 0; std::size_t arenaBytes = 0; @@ -562,6 +632,142 @@ void CheckUnwatchedImport() { #endif } +class PartlyCommitted { +public: + static constexpr std::size_t Bytes = 4 * Block; + + PartlyCommitted() { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(GuestArena::GuestArenaHighestFree_nid_no_patch(Bytes, Block), Bytes, Block)); +#else + void* raw = mmap(nullptr, Bytes + Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + Require(raw != MAP_FAILED, "cannot reserve the partly committed range"); + 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); + memory = reinterpret_cast(aligned); +#endif + Require(memory != nullptr, "cannot reserve the partly committed range"); + } + + ~PartlyCommitted() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, Bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, Bytes); + munmap(memory, Bytes); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(), Bytes); +#endif + } + + PartlyCommitted(const PartlyCommitted&) = delete; + PartlyCommitted& operator=(const PartlyCommitted&) = delete; + + void Commit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(memory + block * Block, Block, PAGE_READWRITE, Block); +#else + Require(mmap(memory + block * Block, Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot commit a block of the partly committed range"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + block * Block, Block); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + void Decommit(std::size_t block) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory + block * Block, Block); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory + block * Block, Block); + Require(mmap(memory + block * Block, Block, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "cannot decommit a block of the partly committed range"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(Address(block), Block); + } + + std::uint8_t* Data() const { return memory; } + std::uint64_t Address(std::size_t block = 0) const { return reinterpret_cast(memory) + block * Block; } + +private: + std::uint8_t* memory = nullptr; +}; + +using Runs = std::vector>; + +std::uint64_t CollectCommitted(const PartlyCommitted& range, Runs& runs) { + BumpCollectEpoch(); + return CollectCommittedWrites(range.Address(), PartlyCommitted::Bytes, runs); +} + +void CheckCommittedRanges() { + PartlyCommitted range; + const auto base = range.Address(); + range.Commit(0); + std::memset(range.Data(), 0x11, Block); + Require(CollectWrites(base, PartlyCommitted::Bytes) == 0, "a partly committed range was collected as a whole"); + Runs runs; + const Runs first{{base, base + Block}}; + const auto synced = CollectCommitted(range, runs); + Require(synced != 0 && runs == first, "the committed part of a partly committed range was not collected"); + Require(UnchangedCommittedSince(runs, synced), "a collected committed part reads as changed"); + + range.Data()[8] = 0x22; + const auto written = CollectCommitted(range, runs); + Require(written > synced && runs == first && !UnchangedCommittedSince(runs, synced) && UnchangedCommittedSince(runs, written), "a CPU write to the committed part was missed"); + Require(MarkWritten(base + 16, 4) != 0 && !UnchangedCommittedSince(first, written), "a driver store into the committed part was missed"); + Require(UnchangedCommittedSince(first, CollectCommitted(range, runs)), "a collect after a driver store reads the committed part as changed"); + + const auto beforeCommit = CollectCommitted(range, runs); + range.Commit(2); + const Runs both{{base, base + Block}, {range.Address(2), range.Address(3)}}; + const auto grown = CollectCommitted(range, runs); + Require(grown != 0 && runs == both, "a block committed inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeCommit), "a block committed inside the range reads as unchanged since before the commit"); + Require(UnchangedCommittedSince(runs, grown), "the committed parts read as changed after their collect"); + + range.Decommit(2); + range.Commit(2); + const auto recommitted = CollectCommitted(range, runs); + Require(recommitted != 0 && runs == both && !UnchangedCommittedSince(runs, grown), "a block decommitted and committed again reads as unchanged"); + + range.Decommit(2); + Require(CollectCommitted(range, runs) != 0 && runs == first, "a decommitted block is still described as committed"); + +#ifdef _WIN32 + HANDLE firstSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + HANDLE secondSection = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Block, nullptr); + struct Sections { + HANDLE first; + HANDLE second; + ~Sections() { + if (first != nullptr) CloseHandle(first); + if (second != nullptr) CloseHandle(second); + } + } sections{firstSection, secondSection}; + Require(firstSection != nullptr && secondSection != nullptr, "cannot create the shared backings of the partly committed range"); + const auto beforeMap = CollectCommitted(range, runs); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, firstSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + const Runs viewed{{base, base + Block}, {range.Address(3), range.Address(4)}}; + const auto mapped = CollectCommitted(range, runs); + Require(mapped != 0 && runs == viewed, "a view mapped inside the range is not described"); + Require(!UnchangedCommittedSince(runs, beforeMap), "a view mapped inside the range reads as unchanged since before the mapping"); + Require(UnchangedCommittedSince(runs, mapped), "a collected view reads as changed"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestArena::GuestArenaMap_nid_postfix(range.Data() + 3 * Block, Block, secondSection, 0, PAGE_READWRITE); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == viewed && !UnchangedCommittedSince(runs, mapped), "a view replaced inside the range reads as unchanged"); + GuestArena::GuestArenaReset_nid_postfix(range.Data() + 3 * Block, Block); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(range.Address(3), Block); + Require(CollectCommitted(range, runs) != 0 && runs == first, "an unmapped view is still described as committed"); +#endif + + range.Decommit(0); + Require(CollectCommitted(range, runs) != 0 && runs.empty() && UnchangedCommittedSince(runs, TrackerGeneration()), "a range with no committed page did not collect as empty"); + Require(!UnchangedCommittedSince(first, 0), "an unknown generation was accepted for the committed parts"); +} + int main() { try { if (!WriteWatched()) { @@ -581,12 +787,14 @@ int main() { #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); + CheckUntrackedHint(); CheckHostHoleInChild(); #endif CheckVersionRanges(); CheckCollectBoundaries(); CheckImportWindow(); CheckUnwatchedImport(); + CheckCommittedRanges(); } catch (const std::exception& error) { std::cerr << "write tracking test failed: " << error.what() << "\n"; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 288ca4e06c..81807e14b2 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/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" @@ -11,6 +12,7 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "Recompiler.hpp" #include "Optimization/ResourceProgram.hpp" +#include "DepthFastClearHarness.hpp" #include "VulkanTestDevice.hpp" #ifdef _WIN32 #ifndef NOMINMAX @@ -28,9 +30,12 @@ #include #include #include +#include +#include #include #include #include +#include #include namespace { @@ -64,11 +69,11 @@ constexpr std::array Destinations{{ {0.5f, 0.25f, 0.75f, 0.5f}, {1.0f, 0.0f, 0.125f, 0.25f}, {0.0625f, 0.875f, 0.5f, 1.0f}, {0.25f, 0.5f, 0.375f, 0.75f} }}; -alignas(256) std::array, 8> Programs{}; +alignas(256) std::array, 16> Programs{}; std::size_t programCount = 0; std::uint16_t Half(float value) { - Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + Require(value == 0.0f || (value >= 0.0078125f && value <= 1.0f), "the test's half values are normal and at most 1"); if (value == 0.0f) return 0; const auto bits = std::bit_cast(value); Require((bits & 0x1fffu) == 0, "the test's half values are exact"); @@ -92,7 +97,7 @@ std::uint32_t Packed16(float value, std::uint32_t exportFormat) { return static_cast(static_cast(std::lround(value * 32767.0f))); } -std::span Program(const Color& color, std::uint32_t exportFormat) { +std::span Program(const Color& color, std::uint32_t exportFormat, std::optional sampleMask = std::nullopt, std::optional packedAlpha = std::nullopt) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; @@ -100,9 +105,14 @@ std::span Program(const Color& color, std::uint32_t exportF code[at++] = 0x7e0002ffu | (vgpr << 17u); code[at++] = value; }; + if (sampleMask) { + move(8, *sampleMask); + code[at++] = 0xf8001084u; + code[at++] = 0x00080000u; + } if (exportFormat != Float32Export) { move(4, Packed16(color[0], exportFormat) | (Packed16(color[1], exportFormat) << 16u)); - move(5, Packed16(color[2], exportFormat) | (Packed16(color[3], exportFormat) << 16u)); + move(5, Packed16(color[2], exportFormat) | ((packedAlpha ? *packedAlpha : Packed16(color[3], exportFormat)) << 16u)); code[at++] = 0xf8001c0fu; code[at++] = 0x00000504u; } else { @@ -172,21 +182,23 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t samples = 1) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; cx[0x31c] = Float16x4Info; - cx[0x31d] = 0; + const auto log2 = static_cast(std::countr_zero(samples)); + cx[0x31d] = (log2 << 12u) | (log2 << 15u); + cx[0x321] = 0; cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); cx[0x3b8] = TiledAttrib3; cx[0x390] = static_cast(block.Address() >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.bytes == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.elementBytes == 8 && color.samples == samples && color.bytes * samples == block.bytes, "the 16_16_16_16 float target did not decode as a 256x128 RGBA16F surface"); return color; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool alphaToCoverage, std::uint32_t exportFormat, std::span pixelCode, bool dithered = false, bool sampleMaskExport = false) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; const auto target = device.Target(); @@ -222,12 +234,15 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge state.blend = blend; state.blendConstants = {}; state.alphaToCoverage = alphaToCoverage; + state.ditheredAlphaToCoverage = dithered; + state.samples = color.samples; const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, sampleMaskExport ? 0x100u : 0u}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; auto prepared = AgcDriver::Graphics::DecodePixelStageInfo(context, {}); prepared.alphaToCoverage = alphaToCoverage; + prepared.ditheredAlphaToCoverage = dithered; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, {waveSize, 0, {}, std::nullopt, prepared, std::nullopt, pixelMemory}, @@ -240,6 +255,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge fragment.shader.code = snapshot.code; fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->alphaToCoverage = state.alphaToCoverage; + fragment.context.pixel->ditheredAlphaToCoverage = state.ditheredAlphaToCoverage; const ShaderRecompiler::SrtRuntime runtime{}; const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); @@ -255,6 +271,59 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarge device.Draw(state, draw, shaders); } +alignas(256) std::array, 3> DepthOnlyPrograms{}; + +void DepthOnlyCoverage(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& harness, const Block& depth, float alpha, std::size_t slot, bool written, std::uint32_t enable = 0x3u, float fourth = 0.0f) { + namespace Harness = DepthFastClearHarness; + const Harness::Surface surface{depth.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + auto& code = DepthOnlyPrograms[slot]; + code = {0x7e0802ffu, 0x3e800000u, 0x7e0a02ffu, std::bit_cast(alpha), 0x7e0e02ffu, std::bit_cast(fourth), 0xf8001800u | enable, 0x07060504u, 0xbf810000u}; + const std::span pixelCode(code.data(), 9); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = true; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 3; + pixel.targetExportMapping.fill(0xe4u); + pixel.alphaToCoverage = true; + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {32, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(harness.vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &harness.vertex, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(harness.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, 0, {Harness::Width, Harness::Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.alphaToCoverage = true; + state.renderExtent = {Harness::Width, Harness::Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Harness::Height), static_cast(Harness::Width), -static_cast(Harness::Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Harness::Width, Harness::Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Harness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + const auto passed = Harness::Draw(device, harness, surface, {.depthCompare = VK_COMPARE_OP_LESS}); + char detail[160]; + std::snprintf(detail, sizeof(detail), "a depth-only draw with alpha-to-coverage and a 32_AR alpha of %.2f (export mask 0x%x, fourth channel %.2f) left %u of %u pixels at the cleared depth", alpha, enable, fourth, passed, Harness::Covered); + Require(passed == (written ? 0u : Harness::Covered), detail); +} + std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); device.WaitIdle(); @@ -294,12 +363,108 @@ void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTarge } } +void DitheredCoverage(AgcDriver::VulkanDevice& device) { + struct Case { + std::uint32_t bits; + std::uint32_t format; + std::array masks; + }; + constexpr std::array cases{{ + {0x00000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x80000000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c7fffffu, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3c800000u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3c800001u, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3cffffffu, 9u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x3d000000u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3d000001u, 9u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x3effffffu, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000000u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f000001u, 9u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x3f77ffffu, 9u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x3f780000u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f780001u, 9u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x3f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x7f800000u, 9u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xff800000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7fc00000u, 9u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00000000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00008000u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x000023ffu, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00002400u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002401u, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x000027ffu, 4u, {0x10u, 0x00u, 0x00u, 0x00u}}, + {0x00002800u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x00002801u, 4u, {0x04u, 0x00u, 0x00u, 0x00u}}, + {0x000037ffu, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003800u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003801u, 4u, {0x35u, 0x36u, 0x36u, 0x35u}}, + {0x00003bbfu, 4u, {0xffu, 0xf7u, 0xffu, 0xffu}}, + {0x00003bc0u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003bc1u, 4u, {0xffu, 0x7fu, 0xffu, 0xffu}}, + {0x00003c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x00007c00u, 4u, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x0000fc00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}}, + }}; + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + for (const auto& test : cases) { + blocks.push_back(std::make_unique(layout.Bytes() * 8u)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + const auto multisampled = DecodeTarget(source, 8); + for (std::uint32_t sample = 0; sample < 8; ++sample) { + const Color color{std::ldexp(1.0f, static_cast(sample) - 7), 0, 0, 1}; + Draw(device, multisampled, false, Float32Export, Program(color, Float32Export, 1u << sample), false, true); + } + const auto alpha = test.format == Float32Export ? std::bit_cast(test.bits) : 0.0f; + const Color color{0, 0, 0, alpha}; + const auto half = test.format == Float16Export ? std::optional(static_cast(test.bits)) : std::nullopt; + Draw(device, multisampled, true, test.format, Program(color, test.format, std::nullopt, half), true); + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = multisampled; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = test.masks[2u * (x & 1u) + (y & 1u)]; + const auto expected = x < ScissorWidth ? static_cast(255u - mask) / 1024.0f : 0.0f; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u]); + if (actual == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), "dithered coverage export %u alpha 0x%08x pixel (%u,%u): resolved %.9g, expected %.9g for mask0x%02x", test.format, test.bits, x, y, actual, expected, mask); + throw std::runtime_error(detail); + } + } + programCount = 0; + } + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); } -int main() { +} + +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { + try { + DitheredCoverage(*device); + } 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 eight-sample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + std::puts("dithered eight-sample alpha-to-coverage tests passed"); + return 0; + } + Require(argc == 1, "unknown alpha-to-coverage test argument"); const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); Block block(layout.Bytes()); struct Case { @@ -343,6 +508,13 @@ int main() { Check(*device, block, layout, before, [&](const Color& destination) { return test.written ? source : destination; }, name); programCount = 0; } + const auto harness = DepthFastClearHarness::Compile(*device); + const Block coveredDepth(BlockBytes); + const Block discardedDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, coveredDepth, 0.75f, 0, true); + DepthOnlyCoverage(*device, harness, discardedDepth, 0.25f, 1, false); + const Block fullMaskDepth(BlockBytes); + DepthOnlyCoverage(*device, harness, fullMaskDepth, 0.25f, 2, false, 0xfu, 0.75f); std::puts("alpha-to-coverage tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 6c7664d7bf..18dcef360c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -42,6 +42,7 @@ constexpr std::size_t LayerBytes = 65536; constexpr std::size_t SurfaceBytes = LayerBytes * Layers; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; @@ -59,9 +60,15 @@ constexpr std::array, 3> Triangle{{ }}; alignas(256) std::array, Layers> LoadCodes{}; +alignas(256) constexpr std::array Sample2DCode{ + 0x7e020d00, 0x060202f0, 0x100202ff, 0x3c000000, 0x7e0402ff, 0x3b800000, 0xf09c0108, 0x00610301, + 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000, +}; alignas(256) std::array PixelCode{}; alignas(256) std::array Output{}; +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode); + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -113,13 +120,13 @@ private: std::uint8_t* block = nullptr; }; -std::array ArrayDescriptor(std::uint64_t address, std::uint32_t swizzleMode) { +std::array SurfaceDescriptor(std::uint64_t address, std::uint32_t swizzleMode, std::uint32_t type, std::uint32_t lastLayer) { return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), - 0xfacu | (swizzleMode << 20u) | (Type2DArray << 28u), - Layers - 1u, + 0xfacu | (swizzleMode << 20u) | (type << 28u), + lastLayer, 0u, 0u, 0u, @@ -146,18 +153,27 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co device.WaitIdle(); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { +void Load(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { Output.fill(0xdeadbeefu); - auto& code = LoadCodes[layer]; - code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; std::vector userData(16, 0u); const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); - const auto texture = ArrayDescriptor(surface, swizzleMode); + const std::array pointSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; + std::copy(pointSampler.begin(), pointSampler.end(), userData.begin() + 12); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); Dispatch(device, code, userData); } +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { + auto& code = LoadCodes[layer]; + code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; + Load(device, code, SurfaceDescriptor(surface, swizzleMode, Type2DArray, Layers - 1u)); +} + +void Sample2D(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t swizzleMode) { + Load(device, Sample2DCode, SurfaceDescriptor(surface, swizzleMode, Type2D, 0u)); +} + void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::string& what) { for (std::uint32_t x = 0; x < Threads; ++x) { const auto expected = x < ScissorWidth ? inside : outside; @@ -273,6 +289,24 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t sw LoadLayer(device, surface, slice, swizzleMode); RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); + SampleSliceZero(device, texels, swizzleMode); +} + +void SampleSliceZero(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + DrawSlice(device, surface, 0, swizzleMode, false); + const auto pending = StorageTexture::FindPending(surface, LayerBytes); + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray, "the draw into slice 0 did not render into the image of its array"); + const auto plain = SurfaceDescriptor(surface, swizzleMode, Type2D, 0u); + Require(AgcDriver::Graphics::Texture::CanCopyFrom(*pending, AgcDriver::Graphics::DecodeTextureResource(plain)), "a 2D texture over slice 0 cannot view the array image's first layer"); + Sample2D(device, surface, swizzleMode); + RequireOutput(std::bit_cast(Drawn), Initial, "slice 0 sampled as a 2D texture after the draw"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp new file mode 100644 index 0000000000..23eeb6dcf7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format32x2Uint = 62; +constexpr std::uint32_t Format32x4Uint = 75; +constexpr std::uint32_t FormatBc1Unorm = 169; +constexpr std::uint32_t FormatBc7Unorm = 181; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t Texels = 256; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; +}; + +struct Case { + std::string name; + Shape blocks; + Shape compressed; + std::span store; +}; + +alignas(256) constexpr std::array StoreQuadCode{ + 0x7e28020c, 0x7e2a0300, 0x7e2c020c, 0x7e2e0300, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array StorePairCode{ + 0x7e28020c, 0x7e2a0300, 0x7e3c0300, 0x7e3e0280, 0xf0201308, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf09c0f08, 0x00820402, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, +}; + +std::span Input; +std::span Output; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "bc block copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "bc block copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +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, const Shape& shape) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (shape.format << 20u) | (((shape.width - 1u) & 3u) << 30u), + ((shape.width - 1u) >> 2u) | ((shape.height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor() { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Case& test, std::uint32_t value) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, test.blocks); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = value; + const auto result = Compile(device, test.store, userData); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(test.store.data())); + device.WaitIdle(); +} + +std::uint64_t SurfaceBytes(std::uint8_t* texels, const Shape& shape) { + return AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels, shape))).guestBytes; +} + +void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), bytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); +} + +std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape) { + std::vector userData(24, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); + const auto texture = TextureDescriptor(texels, shape); + 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 auto program = Compile(device, SampleCode, userData); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = 0.0f; + Input[tid * 3u + 1u] = (static_cast(tid) * 4.0f + 1.5f) / static_cast(shape.width); + Input[tid * 3u + 2u] = 1.5f / static_cast(shape.height); + } + std::fill(Output.begin(), Output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + return {Output.begin(), Output.end()}; +} + +std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value) { + GuestBlock block; + auto* texels = block.Data(); + Store(device, texels, test, value); + Settle(device, texels, SurfaceBytes(texels, test.blocks)); + return Sample(device, texels, test.compressed); +} + +void RequireSame(const std::vector& sampled, const std::vector& expected, const std::string& what) { + for (std::size_t word = 0; word < expected.size(); ++word) { + Require(sampled[word] == expected[word], what + ": thread " + std::to_string(word / 4u) + " component " + std::to_string(word % 4u) + " read " + std::to_string(sampled[word]) + ", expected " + std::to_string(expected[word])); + } +} + +void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { + constexpr std::uint32_t First = 0x13572440u; + constexpr std::uint32_t Second = 0x9bdf6840u; + const auto first = Reference(device, test, First); + const auto second = Reference(device, test, Second); + Require(first != second, test.name + ": the two block patterns sample alike"); + GuestBlock block; + auto* texels = block.Data(); + const auto bytes = SurfaceBytes(texels, test.compressed); + Require(bytes == SurfaceBytes(texels, test.blocks), test.name + ": the block view and the compressed view disagree on the surface size"); + Store(device, texels, test, First); + Settle(device, texels, bytes); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of stored blocks"); + Store(device, texels, test, Second); + const auto pending = Sample(device, texels, test.compressed); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes) != nullptr, test.name + ": sampling the compressed view stored the pending blocks to guest memory"); + RequireSame(pending, second, test.name + ": sample of pending blocks"); + Store(device, texels, test, First); + RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of blocks stored again"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock buffers; + Input = std::span(reinterpret_cast(buffers.Data()), Threads * 3u); + Output = std::span(reinterpret_cast(buffers.Data() + 4096u), Threads * 4u); + PendingBlocksCase(*device, {"BC7 through 32x4 uint blocks", {Format32x4Uint, Texels / 4u, Texels / 4u}, {FormatBc7Unorm, Texels, Texels}, StoreQuadCode}); + PendingBlocksCase(*device, {"BC1 through 32x2 uint blocks", {Format32x2Uint, Texels / 4u, Texels / 4u}, {FormatBc1Unorm, Texels, Texels}, StorePairCode}); + std::puts("bc block copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 45364ef783..bf04d4c6f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -93,6 +93,59 @@ void Run(AgcDriver::VulkanDevice& device, bool registrationModules) { } } +void ScalarReads(AgcDriver::VulkanDevice& device) { + constexpr std::array scalarCode{0x34020082u, 0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x7e040208u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + std::array userData{}; + const auto output = BufferDescriptor(Output.data(), sizeof(Output)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + auto request = Request(userData, device.ComputeTarget(Lanes), {}); + request.shader.code = scalarCode; + request.shader.codeAddress = reinterpret_cast(scalarCode.data()); + request.useCache = false; + std::uint32_t expected = 0; + std::memcpy(&expected, Input.data() + 4u, sizeof(expected)); + for (const auto ignoredWord1 : {0u, 0x40000000u}) { + for (const auto word : {0u, 0x31016facu, 0x91b00f2eu, 0xffffffffu}) { + auto input = BufferDescriptor(Input.data(), InputBytes); + input[1] |= ignoredWord1; + input[3] = word; + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(std::all_of(Output.begin(), Output.end(), [expected](std::uint32_t value) { return value == expected; }), "scalar buffer read changed with ignored descriptor word " + Hex(word)); + } + } + constexpr std::array mixedCode{0xf4200200u, 0xfa000004u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0xe0701000u, 0x80010201u, 0x7e060208u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor uses an unsupported type") != std::string::npos, "vector buffer descriptor failed for another reason"); + continue; + } + throw std::runtime_error("a vector or mixed buffer access accepted a non-buffer descriptor type"); + } + for (const auto code : {std::span(Code), std::span(mixedCode)}) { + request.shader.code = code; + request.shader.codeAddress = reinterpret_cast(code.data()); + auto input = BufferDescriptor(Input.data(), InputBytes); + input[1] |= 0x40000000u; + std::copy(input.begin(), input.end(), userData.begin()); + const auto result = ShaderRecompiler::Recompile(request); + try { + device.Dispatch(result, 1, 1, 1); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("buffer descriptor has reserved bits set") != std::string::npos, "vector or mixed buffer descriptor failed for another reason"); + continue; + } + throw std::runtime_error("a vector or mixed buffer access accepted reserved descriptor bits"); + } +} + } int main() { @@ -102,6 +155,7 @@ int main() { for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast(index * 41u + 3u + (index >> 3u)); Run(*device, false); Run(*device, true); + ScalarReads(*device); std::puts("buffer descriptor runtime tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index eae20eb5a1..e9668e78dc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -100,6 +100,9 @@ public: const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, BlockBytes, nullptr); Require(section != nullptr, "cannot create shared test memory"); GuestArena::GuestArenaMap_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); + twin = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + Require(twin != nullptr, "cannot reserve the second mapping of shared test memory"); + GuestArena::GuestArenaMap_nid_postfix(twin, BlockBytes, section, 0, PAGE_READWRITE); CloseHandle(section); } else if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); } else { @@ -130,6 +133,10 @@ public: ~GuestBlock() { GuestAllocations::Mutation().Remove(block); #ifdef _WIN32 + if (twin != nullptr) { + GuestArena::GuestArenaReset_nid_postfix(twin, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(twin, BlockBytes); + } if (watched) { GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); @@ -154,6 +161,7 @@ public: private: bool watched; std::uint8_t* block = nullptr; + std::uint8_t* twin = nullptr; }; std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { @@ -337,18 +345,37 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { } readPending(DccKeys::Uncompressed, false, "a pending fill of the keys with 0x5a, which is no clear code"); Require(fill(0, KeyBytes, 0x10), "the comp-to-single fill of the keys was not recorded"); - readPending(DccKeys::ClearSingle, true, "a pending comp-to-single fill of the keys"); + readPending(DccKeys::ClearSingle, false, "a pending comp-to-single fill of the keys"); + RequireKeys(keys, 0x10, "the keys after a pending comp-to-single fill landed"); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); - readPending(DccKeys::Clear0000, true, "pending 0000 fills over both halves of the keys"); + readPending(DccKeys::Clear0000, false, "pending 0000 fills over both halves of the keys"); + RequireKeys(keys, 0x00, "the keys after pending 0000 fills over both halves landed"); + Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x40), "the 0000 and 0001 fills of the key halves were not recorded"); + readPending(DccKeys::Uncompressed, false, "a pending 0000 fill and a pending 0001 fill over the halves of the keys"); Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half was not recorded"); Dispatch(device, ClearKeysCode, clearTailData, false); readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys and a kernel's 0000 store over the other half"); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill of the keys was not recorded"); + Dispatch(device, ClearKeysCode, clearTailData, false); + Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half over the kernel's store was not recorded"); + readPending(DccKeys::Uncompressed, true, "a pending 0001 fill over half the keys, whose other half a kernel stored over an earlier 0001 fill"); Require(fill(0, KeyBytes, 0x10) && fill(0, KeyBytes, 0x00), "the 0x10 and 0000 fills of the keys were not recorded"); - readPending(DccKeys::Clear0000, true, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + readPending(DccKeys::Clear0000, false, "a pending 0x10 fill of the keys overwritten by a pending 0000 fill"); + RequireKeys(keys, 0x00, "the keys after a pending 0x10 fill and a pending 0000 fill landed"); + Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys under a kernel's store was not recorded"); + Dispatch(device, ClearKeysCode, clearData, false); + Require(fill(0, KeyBytes, 0x40), "the 0001 fill over the kernel's store was not recorded"); + readPending(DccKeys::Clear0001, false, "a pending 0001 fill of the keys over a kernel's 0000 store over a pending 0x10 fill"); + RequireKeys(keys, 0x40, "the keys after a 0x10 fill, a kernel's 0000 store and a 0001 fill landed"); Require(fill(KeyBytes / 2, KeyBytes / 2, 0x10), "the 0x10 fill of the second key half was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00), "the 0000 fill of the first key half was not recorded"); readPending(DccKeys::Uncompressed, false, "a pending 0000 fill over half the keys whose other half holds 0x10"); + Require(fill(KeyBytes / 2, KeyBytes / 2, 0xc0), "the 1111 fill of the second key half was not recorded"); + device.WaitIdle(); + Require(fill(0, KeyBytes / 2, 0xc0), "the 1111 fill of the first key half was not recorded"); + readPending(DccKeys::Clear1111, false, "a pending 1111 fill over half the keys whose other half holds 1111"); + RequireKeys(keys, 0xc0, "the keys after a pending 1111 fill over a half that holds 1111 landed"); Require(fill(0, KeyBytes, 0x10), "the 0x10 fill of the keys was not recorded"); device.WaitIdle(); Require(fill(0, KeyBytes / 2, 0x00) && fill(KeyBytes / 2, KeyBytes / 2, 0x00), "the 0000 fills of the key halves were not recorded"); @@ -651,6 +678,77 @@ void RunImportWindow(AgcDriver::VulkanDevice& device, std::uint8_t* block) { #endif #ifdef _WIN32 +void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + using namespace AgcDriver::GuestMemory; + const auto address = AddressOf(block); + std::memset(block, 0x55, BlockBytes); + auto descriptor = TextureDescriptor(block, nullptr); + descriptor[2] = ((Side - 1u) >> 2u) | ((2 * Side - 1u) << 14u); + descriptor[6] = 0; + descriptor[7] = 0; + std::vector userData(16, 0u); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 4); + std::lock_guard gpu(GpuMutex()); + BumpCollectEpoch(); + Dispatch(device, WriteCode, userData); + const auto image = StorageTexture::FindPending(address, BlockBytes); + Require(image != nullptr && image->GuestBytes() == BlockBytes, "hinted import: the two-block image has no pending results"); + if (Watched(address, BlockBytes)) { + std::puts("the write-watched image stayed watched: its untracked hint is not tested"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + return; + } + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + for (int settle = 0; settle < 2; ++settle) { + BumpCollectEpoch(); + image->Refresh(); + } + const auto version = image->Version(); + const auto compares = StorageTexture::UntrackedCompares(); + for (int epoch = 0; epoch < 50; ++epoch) { + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == version, "hinted import: an unchanged image was uploaded again"); + } + char message[192]; + std::snprintf(message, sizeof(message), "hinted import: 50 collect epochs without a store compared the image %llu times", static_cast(StorageTexture::UntrackedCompares() - compares)); + Require(StorageTexture::UntrackedCompares() == compares, message); + block[SurfaceBytes + 0x100] = 0x33; + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != version, "hinted import: a CPU store into the imported image was missed"); + const auto stored = StorageTexture::UntrackedCompares(); + Require(stored > compares, "hinted import: the CPU store was found without a compare"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - stored <= 1, "hinted import: the stored image did not settle"); + const auto settled = image->Version(); + block[SurfaceBytes + 0x200] = 0x44; + std::array marked{}; + std::size_t count = marked.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast(address + SurfaceBytes), 4096, marked.data(), &count, true) && count == 1, "hinted import: the store to hide was not marked"); + MarkWritten(address + SurfaceBytes + 0x200, 1); + BumpCollectEpoch(); + Require(!image->Refresh() && image->Version() != settled, "hinted import: a recorded store without a write-watch mark was missed"); + Dispatch(device, WriteCode, userData); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: the second write left no pending results"); + BumpCollectEpoch(); + image->Refresh(); + const auto pendingVersion = image->Version(); + const auto beforeRewrite = StorageTexture::UntrackedCompares(); + auto* rewritten = reinterpret_cast(block + SurfaceBytes + 0x2000); + *rewritten = *rewritten; + BumpCollectEpoch(); + Require(image->Refresh() && image->Version() == pendingVersion, "hinted import: a store of the bytes already there uploaded the image"); + Require(StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: a store of the bytes already there did not cost exactly one compare"); + Require(StorageTexture::FindPending(address, BlockBytes) == image, "hinted import: a store of the bytes already there dropped the pending results"); + BumpCollectEpoch(); + Require(image->Refresh() && StorageTexture::UntrackedCompares() - beforeRewrite == 1, "hinted import: the image was compared again without a store"); + StorageTexture::FlushPending(address, BlockBytes, nullptr, "test hinted import"); + device.WaitIdle(); + Require(Count(block, SurfaceBytes, 0xff) == Threads * 4u, "hinted import: the pending results were lost"); + Require(block[SurfaceBytes + 0x100] == 0x33 && block[SurfaceBytes + 0x200] == 0x44, "hinted import: the write-back lost the CPU store"); +} + void RunSharedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { using namespace AgcDriver::GuestMemory; const auto address = AddressOf(block); @@ -691,6 +789,7 @@ int main() { std::optional shared; std::optional window; std::optional pendingSingle; + std::optional hinted; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; block.emplace(false); @@ -734,6 +833,9 @@ int main() { #ifndef _WIN32 window.emplace(true); RunImportWindow(*device, window->Data()); +#else + hinted.emplace(true); + RunHintedImport(*device, hinted->Data()); #endif } else { std::puts("guest memory has no write watch: a write-watched surface is not tested"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp index d54ab03997..f672de40ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBiasPerFace.cpp @@ -1,9 +1,19 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include #include +#include +#include #include #include #include @@ -124,6 +134,142 @@ void Check(bool frontDrawn, bool backDrawn, std::string_view what) { } } +class GuestBlock { +public: + static constexpr std::size_t BlockBytes = 65536; + + 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, "cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + 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() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +void DepthDrawRecipe(AgcDriver::VulkanDevice& device) { + constexpr std::uint32_t TargetWidth = 128; + constexpr std::uint32_t TargetHeight = 128; + const AgcDriver::Graphics::ColorTargetLayout colorLayout(TargetWidth, TargetHeight, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + + const GuestBlock guestVertices; + const GuestBlock guestTarget; + alignas(256) static std::array RecipeDepthMemory{}; + alignas(256) static std::array RecipeAlternateDepthMemory{}; + + std::memset(guestTarget.Data(), static_cast(Kept), GuestBlock::BlockBytes); + RecipeDepthMemory.fill(1.0f); + RecipeAlternateDepthMemory.fill(1.0f); + + auto* targetVertices = reinterpret_cast*>(guestVertices.Data()); + for (std::size_t index = 0; index < Corners.size(); ++index) { + targetVertices[index] = {Corners[index][0] * 2.0f / TargetWidth - 1.0f, 1.0f - Corners[index][1] * 2.0f / TargetHeight, SurfaceDepth, 1.0f}; + } + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(targetVertices, 16u, static_cast(Corners.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 = {guestTarget.Address(), {TargetWidth, TargetHeight}, VK_FORMAT_R8G8B8A8_UNORM, colorLayout.Bytes(), 0xe4u, AgcDriver::Graphics::ColorTileMode::RenderTarget}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + state.depthTest = true; + state.depthWrite = false; + state.depthCompare = VK_COMPARE_OP_LESS; + state.depthBias = true; + state.depthBiasConstant = -Bias; + state.depthBiasPerFace = false; + state.backDepthBiasConstant = -Bias; + state.renderExtent = {TargetWidth, TargetHeight}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(TargetHeight), static_cast(TargetWidth), -static_cast(TargetHeight), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {TargetWidth, TargetHeight}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Corners.size()), 0, 1, 0, false}; + + std::shared_ptr recipe; + device.Draw(state, draw, shaders, {}, &recipe); + device.WaitIdle(); + Require(recipe != nullptr, "a direct draw with depth target must produce a recipe"); + Require(recipe->hasDepth, "the recipe must record depth target presence"); + Require(recipe->targetViews.size() == 2, "the recipe must record color and depth views"); + + const auto outcome = device.DrawFromRecipe(state, draw, shaders, {}, recipe); + device.WaitIdle(); + Require(outcome == AgcDriver::RecipeOutcome::Recorded, "drawing with depth from recipe must record successfully"); + + auto mutatedAddressState = state; + mutatedAddressState.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(RecipeAlternateDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0}; + const auto rejectedAddressOutcome = device.DrawFromRecipe(mutatedAddressState, draw, shaders, {}, recipe); + Require(rejectedAddressOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target address changes"); + + auto removedDepthState = state; + removedDepthState.depth.reset(); + const auto rejectedDepthOutcome = device.DrawFromRecipe(removedDepthState, draw, shaders, {}, recipe); + Require(rejectedDepthOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target is removed"); +} + } int main() { @@ -138,6 +284,7 @@ int main() { Check(true, true, "both faces pulled closer"); Draw(*device, -Bias, 0.0f); Check(true, false, "only front faces biased"); + DepthDrawRecipe(*device); std::puts("per-face depth bias tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp index 6f43c74925..0ecf904715 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -88,9 +88,12 @@ void DmaFill(const GuestBlock& htile, std::uint32_t bytes, std::uint32_t pattern constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; -void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t bytes) { +constexpr std::array BufferPatternFillKernel{0xd7460004u, 0x04010c08u, 0xf4280302u, 0xfa000000u, 0xbf8cc07fu, 0x7e00020cu, 0x7e02020du, 0x7e04020eu, 0x7e06020fu, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t bytes, const GuestBlock* patternBuffer = nullptr) { const auto kernel = code.Address(); const auto base = htile.Address(); + const auto source = patternBuffer != nullptr ? patternBuffer->Address() : 0; Submit({ Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), @@ -102,10 +105,10 @@ void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), Pm4Packet(0x76, {0x242, bytes / 16u}), Pm4Packet(0x76, {0x243, 0x4bfacu}), - Pm4Packet(0x76, {0x244, 0}), - Pm4Packet(0x76, {0x245, 0}), - Pm4Packet(0x76, {0x246, 0}), - Pm4Packet(0x76, {0x247, 0}), + Pm4Packet(0x76, {0x244, Low(source)}), + Pm4Packet(0x76, {0x245, source != 0 ? (High(source) & 0xffffu) | (16u << 16u) : 0}), + Pm4Packet(0x76, {0x246, source != 0 ? 1u : 0}), + Pm4Packet(0x76, {0x247, source != 0 ? 0x4dfacu : 0}), Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), }); } @@ -144,6 +147,21 @@ void DispatchFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& sha Require(Draw(device, shaders, surface, {}) == 0u, "the depth written after the compute fill clear was lost"); } +void DispatchBufferPatternFillClearsDepth(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), BufferPatternFillKernel.data(), sizeof(BufferPatternFillKernel)); + auto& pattern = NewBlock(); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + std::memset(pattern.Data(), 0xff, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {}) == 0u, "a compute fill with the buffer pattern 0xffffffff (ZMask 0xf) cleared the depth"); + std::memset(pattern.Data(), 0, 16); + DispatchFill(code, htile, HtileFillBytes, &pattern); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill with the buffer pattern 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); +} + void DepthOnlyFillKeepsStencil(AgcDriver::VulkanDevice& device, const Shaders& shaders) { const auto& htile = NewBlock(); const Surface surface{NewBlock().Address(), NewBlock().Address(), htile.Address(), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; @@ -168,7 +186,10 @@ int main() { DmaFillClearsDepth(*device, shaders); DepthOnlyFillKeepsStencil(*device, shaders); const bool computeRan = StoresFills(*device, NewBlock()); - if (computeRan) DispatchFillClearsDepth(*device, shaders); + if (computeRan) { + DispatchFillClearsDepth(*device, shaders); + DispatchBufferPatternFillClearsDepth(*device, shaders); + } else std::printf("compute fill not run: the device does not store fills into imported guest memory\n"); LibcRunShutdown_nid_postfix(); std::puts(computeRan ? "depth fast clear submit tests passed" : "depth fast clear submit tests passed without the compute fill"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index a04e690b2c..797f086287 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -225,15 +226,7 @@ void FillLayer(std::uint8_t* layer, float depth) { for (std::size_t offset = 0; offset < LayerBytes; offset += sizeof(depth)) std::memcpy(layer + offset, &depth, sizeof(depth)); } -void Run(const Context& base, const WatchedBlock& block) { - TextureDetiler detiler(base); - auto context = base; - context.detiler = &detiler; - const auto address = block.Address(); - FillLayer(block.Data(), 1.0f); - FillLayer(block.Data() + LayerBytes, 0.25f); - Require(DepthSliceBytes({Side, Side}, 4u) == LayerBytes, "the depth slice of the test surface is not one layer of the guest block"); - DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); +GuestTextureResource ArrayOver(std::uint64_t address) { GuestTextureResource resource{}; resource.baseAddress = address; resource.width = Side; @@ -247,8 +240,19 @@ void Run(const Context& base, const WatchedBlock& block) { resource.dstSelY = 5; resource.dstSelZ = 6; resource.dstSelW = 7; - const std::array words{}; - const VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + return resource; +} + +constexpr std::array words{}; +constexpr VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + +void Run(const Context& context, const WatchedBlock& block) { + const auto address = block.Address(); + FillLayer(block.Data(), 1.0f); + FillLayer(block.Data() + LayerBytes, 0.25f); + Require(DepthSliceBytes({Side, Side}, 4u) == LayerBytes, "the depth slice of the test surface is not one layer of the guest block"); + DepthSurfaceView(context, DepthTarget{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + const auto resource = ArrayOver(address); watchedBegin = address + LayerBytes; watchedEnd = address + BlockBytes; AgcDriver::GuestMemory::SetFlushHook(&CountReads); @@ -295,6 +299,20 @@ void Run(const Context& base, const WatchedBlock& block) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void RecordedUploads(const Context& context, const WatchedBlock& block) { + const auto resource = ArrayOver(block.Address()); + Recorder recorder(context); + recorder.Activate(); + for (int round = 1; round <= 3; ++round) { + FillLayer(block.Data() + LayerBytes, 0.125f * static_cast(round)); + const auto before = recorder.OpenKeptBytes(); + Require(DepthSurfaceTexture(context, words, resource, components) != nullptr, "the 2D array over the depth surface was not sampled through its depth planes while recording"); + const auto kept = recorder.OpenKeptBytes() - before; + Require(kept >= LayerBytes, "the recorded upload of a written guest layer added " + std::to_string(kept) + " bytes to the open batch's kept bytes, less than the " + std::to_string(LayerBytes) + " bytes of the layer it holds until the batch completes"); + } + recorder.Sync(); +} + } int main() { @@ -312,9 +330,13 @@ int main() { return Skipped; } { + TextureDetiler detiler(device->GetContext()); + auto context = device->GetContext(); + context.detiler = &detiler; WatchedBlock block; Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); - Run(device->GetContext(), block); + Run(context, block); + RecordedUploads(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 2c0031e06c..ec58a19d3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -243,10 +243,10 @@ void Run(Context context, std::uint8_t* block) { layeredStencil.depthOrLastArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); layeredDepth.depthOrLastArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the depth plane was accepted"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); layeredDepth.depthOrLastArray = 0; layeredDepth.baseArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "storage image access to depth/stencil surface", "a 2D array view of layer 1 of the depth plane was accepted"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "through a 2D array view of slices 1-0", "a 2D array view of layer 1 of the depth plane was accepted"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp index 9b8dba980d..cf2517f292 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -39,7 +39,8 @@ constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t Type2DArray = 13; constexpr std::uint32_t TileLinear = 0; constexpr std::uint32_t TileStandard256B = 1; -constexpr std::size_t BlockBytes = 1u << 20u; +constexpr std::uint32_t TileZ64KBX = 0x18; +constexpr std::size_t BlockBytes = 2u << 20u; constexpr std::size_t StencilOffset = 1u << 16u; constexpr std::size_t OutputOffset = 2u << 16u; constexpr std::size_t MultisampledOffset = 4u << 16u; @@ -48,6 +49,8 @@ constexpr std::size_t SampledOffset = 8u << 16u; constexpr std::size_t LaterDepthOffset = 10u << 16u; constexpr std::size_t ArrayDepthOffset = 12u << 16u; constexpr std::size_t ArrayStencilOffset = 13u << 16u; +constexpr std::size_t ArrayOffset = 16u << 16u; +constexpr std::uint32_t ArrayLayers = 3; alignas(256) constexpr std::array ScaleDepthCode{ 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, @@ -247,13 +250,77 @@ void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std: RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the one-layer 2D array view's depth"); RunPlane(device, layer(AddStencilLayerCode, stencil, Format8Uint, 0.0f), AddStencilLayerCode, output, 5u, "stencil dispatch through a one-layer 2D array view"); RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the one-layer 2D array view's stencil"); +} +ShaderRecompiler::RecompileResult LayerProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* layers, std::uint32_t layer) { std::vector userData(16, 0u); - const auto texture = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(layers, Format32Float, Type2DArray, TileZ64KBX, ArrayLayers - 1u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - userData[14] = std::bit_cast(1.0f / 64.0f); - const auto program = Compile(device, StoreLayerCode, userData); - ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); }, "storage image access to depth/stencil surface", "a two-layer 2D array storage view of the depth plane"); + userData[12] = layer; + userData[14] = std::bit_cast(1.0f / 128.0f); + return Compile(device, code, userData); +} + +void StoreLayer(AgcDriver::VulkanDevice& device, std::span layers, std::span output, std::uint32_t layer) { + const auto store = LayerProgram(device, StoreLayerCode, output, layers.data(), layer); + device.Dispatch(store, 1, 1, 1, {}, reinterpret_cast(StoreLayerCode.data())); + device.WaitIdle(); +} + +void RequireGuestLayer(std::span layer, bool stored, float rest, float initial, const std::string& what) { + std::vector seen(Threads, 0u); + std::uint32_t kept = 0; + for (std::size_t offset = 0; offset < layer.size(); offset += sizeof(float)) { + float texel = 0.0f; + std::memcpy(&texel, layer.data() + offset, sizeof(texel)); + const auto lane = static_cast(texel * 128.0f); + if (stored && texel < 0.25f && texel != initial && static_cast(lane) / 128.0f == texel) { + ++seen[lane]; + continue; + } + if (texel == rest) { + ++kept; + continue; + } + Require(texel == initial, what + ": guest texel at offset " + std::to_string(offset) + " is " + std::to_string(texel) + ", expected " + std::to_string(rest) + " or the untouched " + std::to_string(initial)); + } + const auto texels = static_cast(layer.size() / sizeof(float)); + const auto expected = (rest == initial ? texels : Width * Height) - (stored ? Threads : 0u); + Require(kept == expected, what + ": " + std::to_string(kept) + " guest texels hold " + std::to_string(rest) + ", expected " + std::to_string(expected)); + if (!stored) return; + for (std::uint32_t lane = 0; lane < Threads; ++lane) Require(seen[lane] == 1u, what + ": the value stored by lane " + std::to_string(lane) + " is in guest memory " + std::to_string(seen[lane]) + " times"); +} + +void LayeredArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + const auto sliceBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(sliceBytes == 1u << 16u, "a depth slice of the test extent is not one 64 KiB block"); + const std::span layers(block + ArrayOffset, sliceBytes * ArrayLayers); + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const auto guestLayer = [&](std::uint32_t layer) { return layers.subspan(sliceBytes * layer, sliceBytes); }; + const float untouched = 0.875f; + const float guestDepth = 0.75f; + for (std::uint32_t layer = 0; layer < ArrayLayers; ++layer) { + const float value = layer == 2 ? guestDepth : untouched; + for (std::size_t offset = 0; offset < sliceBytes; offset += sizeof(float)) std::memcpy(guestLayer(layer).data() + offset, &value, sizeof(float)); + } + const Surface first{reinterpret_cast(guestLayer(0).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface second{reinterpret_cast(guestLayer(1).data()), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, first, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the first layer's depth producer did not cover its surface"); + Require(Draw(device, shaders, second, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the second layer's depth producer did not cover its surface"); + + StoreLayer(device, layers, output, 1); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the values a 2D array storage view stored to the second layer's surface"); + Require(Draw(device, shaders, first, {.depthCompare = VK_COMPARE_OP_GREATER}) == 0, "a store to the second layer changed the first layer's surface"); + StoreLayer(device, layers, output, 2); + RequireRow(Draw(device, shaders, second, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second layer's surface after a store to the third layer"); + + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(layers.data()), layers.size(), nullptr, "test"); + device.WaitIdle(); + RequireGuestLayer(guestLayer(0), false, 0.5f, untouched, "the first layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(1), true, 0.5f, untouched, "the second layer in guest memory after the flush"); + RequireGuestLayer(guestLayer(2), true, guestDepth, guestDepth, "the third layer in guest memory after the flush"); } void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) { @@ -334,6 +401,7 @@ int main() { const auto shaders = DepthFastClearHarness::Compile(*device); StoreBackCase(*device, shaders, block.Data()); ArrayViewCase(*device, shaders, block.Data()); + LayeredArrayViewCase(*device, shaders, block.Data()); MultisampledCase(*device, shaders, block.Data()); TwoViewsCase(*device, block.Data()); SampledCopyCase(*device, shaders, block.Data()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 93c8237c2e..23fbd3392b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -18,6 +18,7 @@ namespace { using AgcDriver::Graphics::Require; using AgcDriver::DriverDetail::DrawMiss; +using AgcDriver::DriverDetail::DrawKeySightings; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; @@ -48,7 +49,7 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawTwice() { +std::vector DrawThrice() { const auto color = reinterpret_cast(ColorMemory.data()); const auto vertex = reinterpret_cast(VertexCode.data()); const auto pixel = reinterpret_cast(PixelCode.data()); @@ -83,28 +84,41 @@ std::vector DrawTwice() { for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); Append(commands, 0x2d, {3, 2}); Append(commands, 0x2d, {3, 2}); + Append(commands, 0x2d, {3, 2}); return commands; } -void SecondDrawHitsTheCache(bool libraries) { +void ThirdDrawHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - auto commands = DrawTwice(); + auto commands = DrawThrice(); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); AgcDriverWaitIdle_nid_postfix(); const auto counters = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; const auto layout = counters.misses[static_cast(DrawMiss::Layout)]; - Require(counters.lookups == 2 && counters.absent == 1, mode + ": the two draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 2 lookups with 1 absent"); + Require(counters.lookups == 3 && counters.absent == 2, mode + ": the three draws made " + std::to_string(counters.lookups) + " draw-cache lookups (" + std::to_string(counters.absent) + " without an entry), expected 3 lookups with 2 absent"); + Require(counters.inserts == 1, mode + ": the three draws inserted " + std::to_string(counters.inserts) + " draw entries, expected 1: a key is cached the second time it is seen"); Require(layout == 0, mode + ": the repeated draw missed the draw cache on its push offset (" + std::to_string(layout) + " layout misses)"); Require(counters.hits == 1, mode + ": the repeated draw did not reuse the cached draw (" + std::to_string(counters.hits) + " hits)"); } +void SightingsKeepCollidingKeys() { + DrawKeySightings sightings(DrawKeySightings::Ways); + Require(!sightings.SeenBefore(0) && sightings.SeenBefore(0), "draw key 0 was not admitted at its second sight"); + Require(!sightings.SeenBefore(11) && !sightings.SeenBefore(12), "an unseen draw key was admitted"); + Require(sightings.SeenBefore(11) && sightings.SeenBefore(12) && sightings.SeenBefore(11), "two keys in one bucket, drawn in turn, were not admitted at their second sight"); + Require(!sightings.SeenBefore(13) && !sightings.SeenBefore(14), "an unseen draw key was admitted"); + Require(!sightings.SeenBefore(0), "the oldest key of a full bucket was kept over the newest"); + Require(sightings.SeenBefore(12) && sightings.SeenBefore(13) && sightings.SeenBefore(14), "a full bucket lost a key newer than the one it replaced"); +} + } int main() { try { + SightingsKeepCollidingKeys(); bool libraries = false; { const auto device = OpenVulkanTestDevice(); @@ -112,7 +126,7 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; - SecondDrawHitsTheCache(libraries); + ThirdDrawHitsTheCache(libraries); AgcDriverShutdown_nid_postfix(); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp index cb9823b3a0..b00770b08e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F64FmaSubnormals.cpp @@ -135,7 +135,7 @@ std::vector Recompile(bool float64DenormPreserve) { request.target.float64DenormPreserve = float64DenormPreserve; request.layout = {0, 0, 0, 128}; request.useCache = false; - return ShaderRecompiler::Recompile(request).spirv; + return ShaderRecompiler::Recompile(request).spirv.Copy(); } bool HasDenormPreserve64(std::span words) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp new file mode 100644 index 0000000000..21078c04c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertureSources.cpp @@ -0,0 +1,240 @@ +#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; +using Words = std::vector; + +constexpr std::uint32_t SharedBase = 235u; +constexpr std::uint32_t SharedLimit = 236u; +constexpr std::uint32_t PrivateBase = 237u; +constexpr std::uint32_t PrivateLimit = 238u; +constexpr std::array Sources{SharedBase, SharedLimit, PrivateBase, PrivateLimit}; +constexpr std::array SourceNames{"src_shared_base", "src_shared_limit", "src_private_base", "src_private_limit"}; +constexpr std::array SourceValues{0x8000000000000000ull, 0x80000000ffffffffull, 0x7000000000000000ull, 0x70000000ffffffffull}; +constexpr std::uint32_t LaneDwords = 32u; +constexpr std::uint32_t LdsDwords = 256u; +constexpr std::uint32_t ScratchDwords = 4u; +constexpr std::uint32_t ScratchOffset = 4u; +constexpr std::uint32_t SharedData = 0x5eed0000u; +constexpr std::uint32_t PrivateData = 0x51de0000u; +constexpr std::uint32_t WaitCnt0 = 0xbf8c0070u; +constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t InlineZero = 128u; +constexpr std::uint32_t InlineOne = 129u; +constexpr std::uint32_t Literal = 255u; +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; +constexpr std::uint32_t CndmaskOp = 0x01u; +constexpr std::uint32_t LshlrevOp = 0x1au; +constexpr std::uint32_t AddNcOp = 0x25u; +constexpr std::uint32_t FlatStoreDword = 0x1cu; +constexpr std::uint32_t FlatLoadDword = 0x0cu; +constexpr std::uint32_t FlatSegment = 0u; +constexpr std::uint32_t ScratchSegment = 1u; +constexpr std::uint32_t CmpLeU64 = 0xe3u; +constexpr std::uint32_t CmpGeU64 = 0xe6u; +constexpr std::uint32_t LaneOffset = 1u; +constexpr std::uint32_t SharedPointer = 3u; +constexpr std::uint32_t SharedValue = 5u; +constexpr std::uint32_t SharedFlatRead = 6u; +constexpr std::uint32_t PrivatePointer = 7u; +constexpr std::uint32_t PrivateValue = 9u; +constexpr std::uint32_t LdsRead = 10u; +constexpr std::uint32_t ScratchRead = 11u; +constexpr std::uint32_t EdgePointer = 12u; +constexpr std::uint32_t PastPointer = 14u; +constexpr std::uint32_t Result = 16u; +constexpr std::uint32_t PrivateFlatRead = 17u; +constexpr std::uint32_t One = 20u; +constexpr std::uint32_t NarrowReads = 8u; +constexpr std::uint32_t WideReads = 12u; +constexpr std::uint32_t PastHigh = 20u; + +struct Compare { + std::uint32_t op; + std::uint32_t source; + std::uint32_t pointer; + std::uint32_t expected; + const char* name; +}; + +constexpr std::array Compares{{ + {CmpLeU64, SharedBase, SharedPointer, 1u, "src_shared_base <= shared pointer"}, + {CmpGeU64, SharedLimit, SharedPointer, 1u, "src_shared_limit >= shared pointer"}, + {CmpLeU64, SharedBase, PrivatePointer, 0u, "src_shared_base <= private pointer"}, + {CmpLeU64, PrivateBase, PrivatePointer, 1u, "src_private_base <= private pointer"}, + {CmpGeU64, PrivateLimit, PrivatePointer, 1u, "src_private_limit >= private pointer"}, + {CmpGeU64, PrivateLimit, SharedPointer, 0u, "src_private_limit >= shared pointer"}, + {CmpGeU64, SharedLimit, EdgePointer, 1u, "src_shared_limit >= src_shared_limit"}, + {CmpGeU64, SharedLimit, PastPointer, 0u, "src_shared_limit >= the first address past it"}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t ScalarMove(std::uint32_t opcode, std::uint32_t sdst, std::uint32_t source) { + return 0xbe800000u | (sdst << 16u) | (opcode << 8u) | source; +} + +std::uint32_t VectorMove(std::uint32_t vdst, std::uint32_t source) { + return 0x7e000200u | (vdst << 17u) | source; +} + +std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t vdst, std::uint32_t src0, std::uint32_t vsrc1) { + return (opcode << 25u) | (vdst << 17u) | (vsrc1 << 9u) | src0; +} + +void EmitFlat(Words& code, std::uint32_t segment, std::uint32_t op, std::uint32_t vdst, std::uint32_t data, std::uint32_t address) { + code.push_back(0xdc000000u | (op << 18u) | (segment << 14u)); + code.push_back((vdst << 24u) | (0x7du << 16u) | (data << 8u) | address); +} + +void EmitOutputStore(Words& code, std::uint32_t vdata, std::uint32_t slot) { + code.push_back(0xe0701000u | (slot * 4u)); + code.push_back(0x80000000u | LaneOffset | (vdata << 8u)); +} + +Words Kernel() { + Words code{Vop2(LshlrevOp, LaneOffset, 135u, 0u)}; + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(ScalarMove(MovB32, NarrowReads + index, Sources[index])); + code.push_back(ScalarMove(MovB64, WideReads + index * 2u, Sources[index])); + } + for (std::uint32_t index = 0; index < 12u; ++index) { + code.push_back(VectorMove(Result, NarrowReads + index)); + EmitOutputStore(code, Result, index); + } + code.push_back(Vop2(LshlrevOp, SharedPointer, 130u, 0u)); + code.push_back(VectorMove(SharedPointer + 1u, WideReads + 1u)); + code.push_back(Vop2(AddNcOp, SharedValue, Literal, 0u)); + code.push_back(SharedData); + code.push_back(VectorMove(PrivatePointer, InlineZero + ScratchOffset)); + code.push_back(VectorMove(PrivatePointer + 1u, WideReads + 5u)); + code.push_back(Vop2(AddNcOp, PrivateValue, Literal, 0u)); + code.push_back(PrivateData); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, SharedValue, SharedPointer); + EmitFlat(code, FlatSegment, FlatStoreDword, 0u, PrivateValue, PrivatePointer); + code.push_back(WaitCnt0); + code.push_back(0xd8d80000u); + code.push_back((LdsRead << 24u) | SharedPointer); + EmitFlat(code, ScratchSegment, FlatLoadDword, ScratchRead, 0u, PrivatePointer); + EmitFlat(code, FlatSegment, FlatLoadDword, SharedFlatRead, 0u, SharedPointer); + EmitFlat(code, FlatSegment, FlatLoadDword, PrivateFlatRead, 0u, PrivatePointer); + code.push_back(WaitCnt0); + EmitOutputStore(code, LdsRead, 12u); + EmitOutputStore(code, ScratchRead, 13u); + EmitOutputStore(code, SharedFlatRead, 14u); + EmitOutputStore(code, PrivateFlatRead, 15u); + code.push_back(VectorMove(EdgePointer, WideReads + 2u)); + code.push_back(VectorMove(EdgePointer + 1u, WideReads + 3u)); + code.push_back(0x80000000u | (PastHigh << 16u) | (InlineOne << 8u) | (WideReads + 3u)); + code.push_back(VectorMove(PastPointer, InlineZero)); + code.push_back(VectorMove(PastPointer + 1u, PastHigh)); + code.push_back(VectorMove(One, InlineOne)); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + const auto& compare = Compares[index]; + code.push_back(0x7c000000u | (compare.op << 17u) | (compare.pointer << 9u) | compare.source); + code.push_back(Vop2(CndmaskOp, Result, InlineZero, One)); + EmitOutputStore(code, Result, 16u + index); + } + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + code.push_back(0xd5010000u | Result); + code.push_back(InlineZero | (InlineOne << 9u) | (Sources[index] << 18u)); + EmitOutputStore(code, Result, 24u + index); + } + code.push_back(EndProgram); + return code; +} + +void Expect(const std::vector& output, std::uint32_t waveSize, std::uint32_t lane, std::uint32_t slot, std::uint32_t expected, const std::string& what) { + const auto read = output[lane * LaneDwords + slot]; + Require(read == expected, "flat aperture sources wave" + std::to_string(waveSize) + ": lane " + std::to_string(lane) + " " + what + " read " + Hex(read) + ", expected " + Hex(expected)); +} + +void Check(const std::vector& output, std::uint32_t waveSize) { + for (std::uint32_t lane = 0; lane < waveSize; ++lane) { + for (std::uint32_t index = 0; index < Sources.size(); ++index) { + const auto value = SourceValues[index]; + const std::string name = SourceNames[index]; + Expect(output, waveSize, lane, index, static_cast(value), "s_mov_b32 of " + name); + Expect(output, waveSize, lane, 4u + index * 2u, static_cast(value), "s_mov_b64 of " + name + " low"); + Expect(output, waveSize, lane, 5u + index * 2u, static_cast(value >> 32u), "s_mov_b64 of " + name + " high"); + const auto maskBit = waveSize == 32u ? (static_cast(value) >> lane) & 1u : static_cast(value >> lane) & 1u; + Expect(output, waveSize, lane, 24u + index, maskBit, "v_cndmask_b32 with " + name + " as the lane mask"); + } + Expect(output, waveSize, lane, 12u, SharedData + lane, "ds_read_b32 after a flat store through src_shared_base"); + Expect(output, waveSize, lane, 13u, PrivateData + lane, "scratch_load_dword after a flat store through src_private_base"); + Expect(output, waveSize, lane, 14u, SharedData + lane, "flat_load_dword through src_shared_base"); + Expect(output, waveSize, lane, 15u, PrivateData + lane, "flat_load_dword through src_private_base"); + for (std::uint32_t index = 0; index < Compares.size(); ++index) { + Expect(output, waveSize, lane, 16u + index, Compares[index].expected, Compares[index].name); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + std::vector output(waveSize * LaneDwords, 0xdeadbeefu); + std::vector userData(8, 0u); + const auto descriptor = BufferDescriptor(output.data(), static_cast(output.size() * 4u)); + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + const auto code = Kernel(); + const std::span codeSpan(code); + const std::array memory{{{reinterpret_cast(codeSpan.data()), std::as_bytes(codeSpan)}}}; + ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + compute.scratchDwords = ScratchDwords; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(codeSpan.data()), codeSpan, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(codeSpan.data())); + device.WaitIdle(); + Check(output, waveSize); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, 32u); + if (device->Target().subgroupSize >= 32u) { + Run(*device, 64u); + } else { + std::printf("skipped wave64, subgroup size %u cannot hold a wave64 in two lanes\n", device->Target().subgroupSize); + } + std::puts("flat aperture sources 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/HostImportWindows.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp new file mode 100644 index 0000000000..57396daef2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportWindows.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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 + +namespace { + +namespace Graphics = AgcDriver::Graphics; +using Graphics::Require; + +constexpr std::uint64_t MiB = 1ull << 20u; +constexpr std::uint64_t WindowBytes = 64 * MiB; +constexpr std::uint64_t BlockBytes = 2 * WindowBytes; +constexpr std::uint64_t OtherBytes = 64 * 1024; +constexpr std::uint64_t GuestAlignment = 64 * 1024; +constexpr std::uint64_t ProbeBytes = 4096; + +std::string Hex(std::uint64_t value) { + char text[32]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +class GuestBlock { +public: + explicit GuestBlock(std::uint64_t size) : bytes(size) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestAlignment, bytes)); +#endif + Require(block != nullptr, "host import windows: cannot allocate a guest block of " + Hex(bytes)); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; + std::uint64_t bytes = 0; +}; + +enum class Outcome : std::uint8_t { NoDevice, NoImport, Passed }; + +Graphics::Context Probe(const AgcDriver::VulkanDevice& device) { + Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return probe; +} + +bool Fill(AgcDriver::VulkanDevice& device, std::uint64_t address) { + const std::array pattern{0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(address, ProbeBytes, pattern); + device.WaitIdle(); + return filled; +} + +Outcome ClearingDropsImports() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + if (!Fill(*device, guest.Address() + WindowBytes)) return Outcome::NoImport; + const auto probe = Probe(*device); + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Graphics::ClearHostImports(device->Device()); + Require(!Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: ClearHostImports kept the window of " + Hex(guest.Address())); + return Outcome::Passed; +} + +Outcome WindowsFollowTheirRange() { + GuestBlock guest(BlockBytes); + auto device = OpenVulkanTestDevice(); + if (!device) return Outcome::NoDevice; + const auto probe = Probe(*device); + const auto address = guest.Address() + WindowBytes + WindowBytes / 2; + if (!Fill(*device, address)) return Outcome::NoImport; + Require(Graphics::HostImportCovers(probe, guest.Address() + WindowBytes, ProbeBytes), "host import windows: the second window of " + Hex(guest.Address()) + " is not imported"); + Require(!Graphics::HostImportCovers(probe, guest.Address(), ProbeBytes), "host import windows: the first window of " + Hex(guest.Address()) + " is imported along with the second"); + const auto first = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(first != 0, "host import windows: the window at " + Hex(address) + " has no serial"); + + GuestBlock other(OtherBytes); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after an unrelated registration"); + Require(Graphics::HostImportSerial(probe, address, ProbeBytes, false) == first, "host import windows: the window at " + Hex(address) + " was dropped by an unrelated registration, although its range still owns it"); + + GuestAllocations::Mutation().Remove(guest.Data()); + GuestAllocations::Mutation().Add(guest.Data(), BlockBytes, true, true, true); + Require(Fill(*device, address), "host import windows: the window at " + Hex(address) + " cannot be filled after its range was mapped again"); + const auto second = Graphics::HostImportSerial(probe, address, ProbeBytes, false); + Require(second != 0 && second != first, "host import windows: the window of the range mapped again kept the serial " + Hex(first)); + return Outcome::Passed; +} + +} + +int main() { + try { + const auto counting = ClearingDropsImports(); + if (counting == Outcome::NoDevice) return VulkanTestSkipped; + if (counting == Outcome::NoImport) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + const auto windows = WindowsFollowTheirRange(); + Require(windows == Outcome::Passed, "host import windows: the device stopped importing guest memory between two scenarios"); + std::puts("host import windows 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/ImageAddressDimension.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp index b2ef1fc6ed..26b6bec54b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAddressDimension.cpp @@ -53,6 +53,12 @@ alignas(256) constexpr std::array VolumeCode{ 0xbf810000, }; +alignas(256) constexpr std::array VolumeArrayCode{ + 0x34020087, 0xe030104c, 0x80001301, 0xe0301050, 0x80001401, 0xe0301054, 0x80001501, 0xbf8c3f70, + 0xf0001f28, 0x00010c13, 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, + 0xe070107c, 0x80000f01, 0xbf810000, +}; + alignas(256) constexpr std::array NarrowCode{ 0x34020087, 0xe0301040, 0x80001001, 0xe0301048, 0x80001101, 0xbf8c3f70, 0xf0900f00, 0x00610c10, 0xe0701070, 0x80000c01, 0xe0701074, 0x80000d01, 0xe0701078, 0x80000e01, 0xbf810000, @@ -216,6 +222,20 @@ void CheckVolume() { } } +void CheckVolumeArray(AgcDriver::VulkanDevice& device) { + FillInput(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Buffer[tid * Words + PlainInput + 5u] = tid % Depth; + Run(device, VolumeArrayCode, VolumeDescriptor()); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{VolumeSliceMarker(tid % Depth), sample.x >> sample.level, sample.y >> sample.level, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + PlainResult + 4u + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_load 2d_array on a 3D texture: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + void CheckNarrow(AgcDriver::VulkanDevice& device) { FillInput(); Run(device, NarrowCode, TextureDescriptor(Texels.data())); @@ -260,6 +280,7 @@ int main() { FillVolume(); Run(*device, VolumeCode, VolumeDescriptor()); CheckVolume(); + CheckVolumeArray(*device); CheckNarrow(*device); CheckNarrowBilinearClamp(*device); std::puts("image address dimension tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index e106d7cfab..bab71fa850 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -24,6 +24,7 @@ constexpr std::uint32_t Threads = 32; 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 Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -126,7 +127,7 @@ void RequireSame(const std::array& forced, const std::array< } } -void RequireDecoded(const std::array& sampled) { +void RequireDecoded(const std::array& sampled, const char* name) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto x = tid % Size; const auto y = (tid / Size) % Size; @@ -134,7 +135,7 @@ void RequireDecoded(const std::array& sampled) { const auto code = Code8(x, y, channel); const float expected = channel == 3u ? static_cast(code) / 255.0f : SrgbToLinear(code); const float value = sampled[tid * 4u + channel]; - Require(std::fabs(value - expected) <= 0.005f, "8_8_8_8_SRGB through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); + Require(std::fabs(value - expected) <= 0.005f, std::string(name) + " through FORCE_DEGAMMA: thread " + std::to_string(tid) + " component " + std::to_string(channel) + " sampled " + std::to_string(value) + ", expected " + std::to_string(expected)); } } } @@ -158,13 +159,21 @@ int main() { FillTexels(); FillInput(true); const auto forcedPoint = Sample(*device, Format8888Srgb, FilterPoint, ForceDegamma); - RequireDecoded(forcedPoint); + RequireDecoded(forcedPoint, "8_8_8_8_SRGB"); RequireSame(forcedPoint, Sample(*device, Format8888Srgb, FilterPoint, 0u), "8_8_8_8_SRGB, point"); FillInput(false); const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma); RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear"); RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly"); - Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); + FillInput(true); + const auto unormPoint = Sample(*device, Format8888UNorm, FilterPoint, ForceDegamma); + RequireDecoded(unormPoint, "8_8_8_8_UNORM"); + RequireSame(unormPoint, forcedPoint, "8_8_8_8_UNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + const auto unormPlain = Sample(*device, Format8888UNorm, FilterPoint, 0u); + 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"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp new file mode 100644 index 0000000000..b410321d5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreArrayThroughVolume.cpp @@ -0,0 +1,173 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Depth = 2; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::size_t TexelBytes = 16384; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0xf0201128, 0x00011714, 0x7e2c0282, 0xf0201128, + 0x00011714, 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 array through volume: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Value(std::uint32_t tid) { + return (tid + 1u) * 0x9e3779b1u; +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array VolumeDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + IdentitySwizzle | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::size_t bytes) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array words{tid % Size, (tid / Size) % Size, tid / (Size * Size), Value(tid)}; + std::copy(words.begin(), words.end(), Input.begin() + tid * 4u); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto texture = VolumeDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(StoreCode); + 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), bytes, nullptr, "test"); + device.WaitIdle(); +} + +void Check(const std::uint8_t* texels) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + const auto& mip = geometry.mips.at(0); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % Size; + const auto y = (tid / Size) % Size; + const auto z = tid / (Size * Size); + std::uint32_t actual = 0; + std::memcpy(&actual, texels + geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u, 4u); + Require(actual == Value(tid), "image_store 2d_array through a 3D T#: texel (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ") is " + Hex(actual) + ", expected " + Hex(Value(tid))); + } + const auto initial = Initial(); + Require(std::equal(texels + geometry.guestBytes, texels + TexelBytes, initial.begin() + static_cast(geometry.guestBytes)), "image_store 2d_array through a 3D T#: a slice past the volume changed bytes after it"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(VolumeDescriptor(texels))); + Require(geometry.guestBytes <= TexelBytes, "image store array through volume: the volume does not fit the texel storage"); + Dispatch(*device, texels, geometry.guestBytes); + Check(texels); + std::puts("image store array through 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/InactiveVertexPointSize.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp new file mode 100644 index 0000000000..9cc96a457c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InactiveVertexPointSize.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 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 ClearMemory{}; +alignas(256) std::array EnabledMemory{}; + +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); +} + +std::vector DrawBoth() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const 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}, + {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}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + 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}); + const auto draw = [&](std::array& memory, std::uint32_t exports) { + 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, 0x69, {0x207, exports}); + Append(commands, 0x2d, {3, 2}); + }; + draw(ClearMemory, 0x01200000); + draw(EnabledMemory, 0x01210000); + return commands; +} + +void InactivePointSizeDrawsLikeDisabled() { + ClearMemory.fill(Kept); + EnabledMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawBoth(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto kept = static_cast(std::count(ClearMemory.begin(), ClearMemory.end(), Kept)); + Require(kept != 0 && kept != ClearMemory.size(), "the triangle without USE_VTX_POINT_SIZE covered " + std::to_string(ClearMemory.size() - kept) + " of " + std::to_string(ClearMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < ClearMemory.size(); ++index) { + Require(EnabledMemory[index] == ClearMemory[index], "byte " + std::to_string(index) + " is " + std::to_string(static_cast(EnabledMemory[index])) + " with USE_VTX_POINT_SIZE on a triangle draw and " + std::to_string(static_cast(ClearMemory[index])) + " without it"); + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + InactivePointSizeDrawsLikeDisabled(); + AgcDriverShutdown_nid_postfix(); + std::puts("USE_VTX_POINT_SIZE on a triangle draw drew the same pixels as the state without it"); + 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/IndirectPatchedStage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp new file mode 100644 index 0000000000..7cf22540a2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectPatchedStage.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +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 PatchedInMemory{}; +alignas(256) std::array PatchedOutMemory{}; +alignas(256) std::array Records{3, 1, 0, 0, 3, 1, Clipped, 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); +} + +std::vector DrawThree() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records.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)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto target = [&](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}); + }; + target(DirectMemory, 0); + Append(commands, 0x2d, {3, 2}); + target(PatchedInMemory, Clipped); + Append(commands, 0x24, {0, BaseVertexLocation, 0, 2}); + target(PatchedOutMemory, 0); + Append(commands, 0x24, {16, BaseVertexLocation, 0, 2}); + return commands; +} + +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 PatchedStagesDrawWithTheirRecordsWords() { + DirectMemory.fill(Kept); + PatchedInMemory.fill(Kept); + PatchedOutMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawThree(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto direct = Covered(DirectMemory); + Require(direct != 0 && direct != DirectMemory.size(), "the direct draw with depth 0 covered " + std::to_string(direct) + " of " + std::to_string(DirectMemory.size()) + " bytes, expected an edge through the target"); + for (std::size_t index = 0; index < DirectMemory.size(); ++index) { + Require(PatchedInMemory[index] == DirectMemory[index], "byte " + std::to_string(index) + " of the indirect draw whose record sets depth 0 over a clipped register value is " + std::to_string(static_cast(PatchedInMemory[index])) + ", the direct draw wrote " + std::to_string(static_cast(DirectMemory[index]))); + } + const auto out = Covered(PatchedOutMemory); + Require(out == 0, "the indirect draw whose record sets a clipped depth over a register value of 0 covered " + std::to_string(out) + " bytes, expected none"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + PatchedStagesDrawWithTheirRecordsWords(); + AgcDriverShutdown_nid_postfix(); + std::puts("CPU-path indirect draws drew with the user data word their records patched"); + 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/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 8b519385a2..77dbe795c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -35,7 +35,7 @@ alignas(256) constexpr std::array VertexCode{ alignas(256) constexpr std::array PixelCode{ 0x7e080f02u, 0x7e0a0f03u, 0xd5430006u, 0x0411ff05u, 0x000000c0u, 0x7e300300u, 0x7e320301u, 0xd742001au, 0x00020000u, 0x7e3602ffu, 0xabcd0000u, 0xd75a001bu, 0x046a0200u, 0xd7420008u, 0x00020100u, 0x7e3802ffu, - 0x12340000u, 0xd75a001cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, + 0x00001234u, 0xd75a401cu, 0x04220300u, 0x7e1e02ffu, 0x4500bc00u, 0xd743001du, 0x043e0140u, 0x7e3c0280u, 0xd75a001eu, 0x04760340u, 0x7e3e0280u, 0xd75a801fu, 0x046a0200u, 0xd7420020u, 0x00020181u, 0xd7420021u, 0x000201c1u, 0xd7420022u, 0x40020000u, 0xd7430023u, 0x043e0000u, 0xe0782000u, 0x80011806u, 0xe0782010u, 0x80011c06u, 0xe0782020u, 0x80012006u, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u, @@ -235,7 +235,7 @@ void Check(std::uint32_t waveSize) { result[1], std::bit_cast(lowX), 0xabcd0000u | HalfOf(SecondStep(Delta(LowX, 2), j, lowX)), - 0x12340000u | HalfOf(SecondStep(Delta(HighX, 2), j, highX)), + 0x1234u | (HalfOf(SecondStep(Delta(HighX, 2), j, highX)) << 16u), std::bit_cast(highY), HalfOf(SecondStep(Delta(HighY, 2), j, highY)), HalfOf(Saturate(SecondStep(Delta(LowX, 2), j, lowX))), diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 8f419f2164..20ab53b94c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -137,7 +137,7 @@ private: std::byte* block = nullptr; }; -bool DeclaresLayer(const std::vector& words) { +bool DeclaresLayer(std::span words) { for (std::size_t at = 5; at < words.size();) { const auto length = words[at] >> 16u; const auto opcode = words[at] & 0xffffu; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp new file mode 100644 index 0000000000..eece8a90ba --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryValueFill.cpp @@ -0,0 +1,180 @@ +#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/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::KeysFillMemo; +using AgcDriver::DriverDetail::MatchMemoryValueBufferFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Format32Uint = 0x00014004u; +alignas(256) std::array Value{}; +alignas(256) std::array Output{}; + +void KeysFillMemoTests() { + using AgcDriver::Graphics::DccKeys; + const auto bytes = [](std::uint32_t byte) { const auto word = byte * 0x01010101u; return std::array{word, word, word, word}; }; + const std::array, 6> codes{{{0x00u, DccKeys::Clear0000}, {0x40u, DccKeys::Clear0001}, {0x80u, DccKeys::Clear1110}, {0xc0u, DccKeys::Clear1111}, {0x20u, DccKeys::ClearRegister}, {0xffu, DccKeys::Uncompressed}}}; + for (const auto& [byte, keys] : codes) { + Require(KeysFillMemo(bytes(byte), false, true) == keys, "a memory-value fill of a key code over keys that are no storage image's was not recorded"); + Require(KeysFillMemo(bytes(byte), true, false) == keys, "a fill of a key code over a storage image's keys was not recorded"); + Require(KeysFillMemo(bytes(byte), true, true) == keys, "a memory-value fill over a storage image's keys was not recorded"); + Require(!KeysFillMemo(bytes(byte), false, false), "a fill over keys that are no storage image's, other than a memory-value fill, was recorded"); + } + Require(!KeysFillMemo(bytes(0x12u), true, true), "a byte that is no key code was recorded"); + auto mixed = bytes(0x40u); + mixed[3] = 0x40404041u; + Require(!KeysFillMemo(mixed, true, true), "a pattern of two different words was recorded"); + mixed = bytes(0x40u); + mixed[0] = 0x40404000u; + Require(!KeysFillMemo(mixed, true, true), "a word of different bytes was recorded"); +} + +std::vector UserData(const std::uint32_t* output, std::uint32_t records, std::uint32_t valueRecords = 16, std::uint32_t valueStride = 0, std::uint32_t format = Format32Uint) { + const auto address = reinterpret_cast(output); + const auto value = reinterpret_cast(Value.data()); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), records, format, static_cast(value), static_cast((value >> 32u) & 0xffffu) | (valueStride << 16u), valueRecords, Format32Uint}; +} + +void MatchTests() { + const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1}; + const std::array packet{0xc0031500u, 4, 1, 1, 0x41u}; + const auto data = UserData(Output.data(), 1024); + const auto fill = MatchMemoryValueBufferFill(packet, Code, data, compute); + Require(fill && fill->address == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->valueAddress == reinterpret_cast(Value.data()), "the memory-value fill kernel was not taken as a fill of 256 dwords"); + for (const std::uint32_t format : {0x00015004u, 0x00016004u}) { + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute).has_value(), "a 32-bit output format was refused"); + } + for (const std::uint32_t format : {0x0000d004u, 0x00027facu, 0x00038004u}) { + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 16, 0, format), compute), "an output format whose elements are not one 32-bit value matched"); + } + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 4), compute).has_value(), "a value descriptor of one 4-byte element was refused"); + Require(MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 4), compute).has_value(), "a value descriptor of 4 bytes was refused"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 0), compute), "a value descriptor without records matched (the load returns 0)"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 3), compute), "a value descriptor shorter than the loaded dword matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 1024, 1, 2), compute), "a value descriptor of one 2-byte element matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); + Require(!MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchMemoryValueBufferFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchMemoryValueBufferFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchMemoryValueBufferFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + auto altered = Code; + altered[5] ^= 1u; + Require(!MatchMemoryValueBufferFill(packet, altered, data, compute), "a different shader matched"); + const auto clamped = MatchMemoryValueBufferFill(packet, Code, UserData(Output.data(), 64), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the output descriptor's records"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups, std::uint32_t wave) { + 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 | (wave == 32 ? 0x8000u : 0u)}); + return words; +} + +void Label(std::vector& words, std::uint32_t* target, std::uint32_t value) { + const auto address = reinterpret_cast(target); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(address), static_cast(address >> 32u), value, 0, 0}); +} + +void Submit(const std::vector& words) { + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); +} + +void Expect(std::size_t first, std::size_t count, std::uint32_t value, const std::string& what) { + for (std::size_t i = 0; i < Output.size(); ++i) { + const auto expected = i >= first && i < first + count ? value : Untouched; + Require(Output[i] == expected, what + ": word " + std::to_string(i) + " is " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } +} + +void ExecuteTests() { + struct Case { + std::uint32_t value, first, records, groups, valueRecords, valueStride, stored; + const char* what; + }; + constexpr std::array cases{{ + {0xffffffffu, 0, 256, 4, 16, 0, 0xffffffffu, "a 16-byte aligned fill of every launched thread"}, + {0u, 0, 256, 2, 16, 0, 0u, "a fill shorter than its records"}, + {0x40404040u, 0, 1000, 4, 16, 0, 0x40404040u, "a fill longer than its launched threads"}, + {0x0c0c0c0cu, 0, 256, 4, 1, 4, 0x0c0c0c0cu, "a fill whose value descriptor holds one 4-byte element"}, + {0x12345678u, 0, 255, 4, 16, 0, 0x12345678u, "a fill whose size is not a multiple of 16 bytes"}, + {0x80808080u, 1, 256, 4, 16, 0, 0x80808080u, "a fill whose start is not 16-byte aligned"}, + {0x20202020u, 0, 0, 4, 16, 0, 0x20202020u, "an empty fill"}, + {0xc0c0c0c0u, 0, 256, 4, 0, 0, 0u, "a fill whose value descriptor has no records (the load returns 0)"}, + }}; + for (const auto wave : {32u, 64u}) { + for (const auto& item : cases) { + Output.fill(Untouched); + Value[0] = item.value; + Submit(Commands(UserData(Output.data() + item.first, item.records, item.valueRecords, item.valueStride), item.groups, wave)); + Expect(item.first, std::min(item.records, item.groups * 64u), item.stored, std::string(item.what) + " (wave " + std::to_string(wave) + ")"); + } + } + Output.fill(Untouched); + Value[0] = 0x11111111u; + auto words = Commands(UserData(Output.data(), 256), 4, 64); + Label(words, &Value[0], 0xc0c0c0c0u); + const auto again = Commands(UserData(Output.data(), 128), 2, 64); + words.insert(words.end(), again.begin(), again.end()); + Submit(words); + for (std::size_t i = 0; i < 256; ++i) { + Require(Output[i] == (i < 128 ? 0xc0c0c0c0u : 0x11111111u), "a fill did not read the value a preceding GPU label stored, at word " + std::to_string(i)); + } +} + +} + +int main() { + try { + KeysFillMemoTests(); + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("memory-value fill 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/MultisampleFillHostOnly.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp new file mode 100644 index 0000000000..1f4a518a9c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MultisampleFillHostOnly.cpp @@ -0,0 +1,251 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.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 +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::ColorTarget; +using AgcDriver::Graphics::Require; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Side = 64; +constexpr std::byte Marker{0xab}; + +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, "multisampled fill: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::byte* Data() { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + + bool Holds(std::byte value, std::size_t offset, std::size_t bytes) const { + return std::all_of(block + offset, block + offset + bytes, [&](std::byte held) { return held == value; }); + } + +private: + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(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 Submit(std::initializer_list> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "multisampled fill: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +constexpr std::array FillKernel{0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + +void DispatchFill(const GuestBlock& code, std::uint64_t base, std::uint32_t bytes, std::uint32_t value) { + const auto kernel = code.Address(); + Submit({ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 64}), + Pm4Packet(0x76, {0x208, 1}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (8u << 1u) | 0x80u}), + Pm4Packet(0x76, {0x240, Low(base)}), + Pm4Packet(0x76, {0x241, (High(base) & 0xffffu) | (16u << 16u)}), + Pm4Packet(0x76, {0x242, bytes / 16u}), + Pm4Packet(0x76, {0x243, 0x4bfacu}), + Pm4Packet(0x76, {0x244, value}), + Pm4Packet(0x76, {0x245, value}), + Pm4Packet(0x76, {0x246, value}), + Pm4Packet(0x76, {0x247, value}), + Pm4Packet(0x15, {bytes / 1024u, 1, 1, 0x41}), + }); +} + +void Settle(const GuestBlock& code, const GuestBlock& sentinel) { + static std::uint32_t round = 0; + ++round; + DispatchFill(code, sentinel.Address(), 1024, round); + std::array stored{}; + AgcDriver::GuestMemory::Read(sentinel.Address(), std::as_writable_bytes(std::span(stored)), 4); + Require(stored == std::array{round, round, round, round}, "the sentinel fill did not land"); +} + +ColorTarget MultisampledTarget(const GuestBlock& color) { + ColorTarget target{}; + target.address = color.Address(); + target.surfaceAddress = target.address; + target.extent = {Side, Side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.elementBytes = 4; + target.bytes = BlockBytes / 4; + target.samples = 4; + return target; +} + +bool FillsTakenAsMultisampledClearsAreNotStored(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::cout << "skipped, the device has no 4x color targets\n"; + return false; + } + 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); + } + } clearTargets{device, context.device}; + auto& code = NewBlock(); + std::memcpy(code.Data(), FillKernel.data(), sizeof(FillKernel)); + + auto& sentinel = NewBlock(); + auto& control = NewBlock(); + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + if (control.Holds(Marker, 0, BlockBytes)) { + std::cout << "skipped, the device does not store compute fills into guest memory\n"; + return false; + } + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of 0 over plain guest memory did not store 0"); + + auto& color = NewBlock(); + const auto target = MultisampledTarget(color); + Require(AgcDriver::Graphics::MultisampledColorBytes(target) == BlockBytes, "the 4x 64x64 RGBA8 target does not take one 64 KiB block"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill of 0 over the whole color memory is not taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes / 2, 0), "a fill over half of the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address() + 4096, BlockBytes - 4096, 0), "a fill starting inside the color memory is taken as the target's clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), 2 * BlockBytes, 0), "a fill reaching past the color memory is taken as the target's clear"); + + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(Marker, 0, BlockBytes), "a compute fill taken as the multisampled target's clear was stored into its color memory"); + DispatchFill(code, color.Address(), BlockBytes / 2, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes / 2) && color.Holds(Marker, BlockBytes / 2, BlockBytes / 2), "a compute fill over half of the color memory was not stored"); + + std::memset(control.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, control.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(control.Holds(std::byte{}, 0, BlockBytes), "a compute fill of plain guest memory was not stored once a multisampled target exists"); + auto& srgbColor = NewBlock(); + auto srgb = MultisampledTarget(srgbColor); + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::MultisampleTargetView(context, srgb) != VK_NULL_HANDLE, "the sRGB multisampled target has no view"); + device.SubmitRecorded(true); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0), "a fill of 0 over an sRGB target's color memory is not taken as its clear"); + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(srgbColor.Address(), BlockBytes, 0x80808080u), "a fill with no exact sRGB clear value is taken as the target's clear"); + std::memset(srgbColor.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, srgbColor.Address(), BlockBytes, 0x80808080u); + Settle(code, sentinel); + Require(srgbColor.Holds(std::byte{0x80}, 0, BlockBytes), "a compute fill with no exact clear value was not stored into the color memory"); + + { + GuestAllocations::Mutation mutation; + mutation.Remove(color.Data()); + mutation.Add(color.Data(), BlockBytes, true, true, true); + } + Require(!AgcDriver::Graphics::MultisampledColorFillAbsorbs(color.Address(), BlockBytes, 0), "a fill over a retired target's color memory is taken as its clear"); + std::memset(color.Data(), static_cast(Marker), BlockBytes); + DispatchFill(code, color.Address(), BlockBytes, 0); + Settle(code, sentinel); + Require(color.Holds(std::byte{}, 0, BlockBytes), "a compute fill over a retired target's color memory was not stored"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const bool tested = FillsTakenAsMultisampledClearsAreNotStored(*device); + LibcRunShutdown_nid_postfix(); + if (!tested) return VulkanTestSkipped; + std::cout << "multisampled fill tests passed\n"; + 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/NggVertexWaveInfo.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp index ad361921fd..1fc52d68ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveInfo.cpp @@ -24,6 +24,11 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; +alignas(256) constexpr std::array GroupInfoVertexCode{ + 0x9380ff02, 0x0009000c, 0x8f009000, 0x94fe00c1, + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, }; @@ -37,16 +42,16 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::span vertexCode) { Pixels.fill(Kept); const auto target = device.Target(); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {waveSize, 8, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} @@ -97,12 +102,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { device.WaitIdle(); } -void Check(std::uint32_t waveSize) { - const auto what = "NGG vertex prologue wave" + std::to_string(waveSize); +void Check(std::uint32_t waveSize, const std::string& source) { + const auto what = "NGG vertex prologue wave" + std::to_string(waveSize) + " (" + source + ")"; for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4; ++channel) { const auto actual = Pixels[pixel * 4u + channel]; - Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the merged wave info must cover every vertex)"); + Require(actual == std::byte{255}, what + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected 255 (EXEC built from the " + source + " must cover every vertex)"); } } } @@ -118,8 +123,10 @@ int main() { return VulkanTestSkipped; } for (const auto waveSize : {64u, 32u}) { - Draw(*device, waveSize); - Check(waveSize); + Draw(*device, waveSize, VertexCode); + Check(waveSize, "merged wave info"); + Draw(*device, waveSize, GroupInfoVertexCode); + Check(waveSize, "group info"); } std::puts("NGG vertex wave info tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp new file mode 100644 index 0000000000..4bdf081efe --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -0,0 +1,339 @@ +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/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 +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +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 int Skipped = 77; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto extensions = function("vkEnumerateDeviceExtensionProperties"); + Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector available(count); + Check(extensions(context.physical, nullptr, &count, available.data()), "vkEnumerateDeviceExtensionProperties"); + auto bytes = AgcDriver::QueryBdaByteFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + auto address = AgcDriver::QueryBdaFeatures(context.physical, function("vkGetPhysicalDeviceFeatures2"), available); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_COMPUTE_BIT) == 0) ++family; + Require(family < count, "no Vulkan compute queue"); + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + 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}; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); + device.ppEnabledExtensionNames = extensionsEnabled.data(); + device.pEnabledFeatures = &enabled; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + context.bufferDeviceAddress = true; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + const Context& GetContext() const { return context; } + +private: + template + TFunction function(const char* name) const { + const auto result = reinterpret_cast(instanceProc(instance, name)); + Require(result != nullptr, name); + return result; + } + + void release() noexcept { + if (context.device != VK_NULL_HANDLE) ClearCachedTextures(context.device); + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; +}; + +class ReservedSurface { +public: + ReservedSurface() { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~ReservedSurface() { + GuestAllocations::Mutation().Remove(block); + Decommit(0, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + } + + ReservedSurface(const ReservedSurface&) = delete; + ReservedSurface& operator=(const ReservedSurface&) = delete; + + void Commit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(block + offset, bytes, PAGE_READWRITE, bytes); +#else + Require(mmap(block + offset, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot commit a page"); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block + offset, bytes); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + for (std::size_t page = 0; page < bytes; page += 4096) static_cast(static_cast(block)[offset + page]); + } + + void Decommit(std::size_t offset, std::size_t bytes) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block + offset, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block + offset, bytes); + Require(mmap(block + offset, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0) != MAP_FAILED, "partially resident snapshot: cannot decommit a page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block + offset), bytes); + } + + std::uint8_t* Data() const { return block; } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +std::atomic surfaceBegin{0}; +std::atomic surfaceEnd{0}; +std::atomic surfaceReads{0}; + +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) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Run(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == SurfaceBytes, "the test descriptor does not describe the reserved surface"); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + surfaceBegin = address; + surfaceEnd = address + SurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool compared, bool same) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the partially resident surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (compared && reads == 0) throw std::runtime_error("the cached snapshot of the partially resident surface was not compared with guest memory " + when); + if (!compared && reads != 0) throw std::runtime_error("the cached snapshot of the partially resident surface was compared with guest memory " + when + " although nothing changed (" + std::to_string(reads) + " reads)"); + if (previous != nullptr && same != (texture == previous)) throw std::runtime_error(std::string("sampling the partially resident surface ") + when + (same ? " replaced the snapshot" : " kept the stale snapshot")); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true, false); + sample("again", false, true); + surface.Data()[0x100] = 0x11; + sample("after a CPU write to its committed part", true, false); + sample("after the re-read of the written part", false, true); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store into its committed part", true, true); + sample("after the compare that followed the driver store", false, true); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was committed", true, true); + sample("after the compare that followed the commit", false, true); + std::memset(surface.Data() + 2 * BlockBytes, 0x33, BlockBytes); + sample("after a CPU write to the newly committed page", true, false); + sample("after the re-read of the newly committed page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + surface.Commit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted and committed again", true, false); + sample("after the re-read of the recommitted page", false, true); + surface.Decommit(2 * BlockBytes, BlockBytes); + sample("after a page inside it was decommitted", true, true); + sample("after the compare that followed the decommit", false, true); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +void PendingStores(const Context& context, ReservedSurface& surface) { + const auto address = surface.Address(); + const auto words = Descriptor(address); + surface.Commit(0, BlockBytes); + std::memset(surface.Data(), 0x5a, BlockBytes); + Recorder recorder(context); + recorder.Activate(); + const auto lookup = [&] { + AgcDriver::GuestMemory::BumpCollectEpoch(); + return CachedSampledTexture(context, words); + }; + auto previous = lookup(); + Require(previous != nullptr && !previous->ViewsStorageImage(), "the pending-store surface did not produce a snapshot"); + Require(lookup() == previous, "the unchanged pending-store surface did not reuse its snapshot"); + for (const bool submitted : {false, true}) { + const auto landed = std::make_shared(false); + const auto value = submitted ? std::byte{0x99} : std::byte{0x77}; + std::vector original(reinterpret_cast(surface.Data()), reinterpret_cast(surface.Data()) + 0x1000); + recorder.NotePendingWrite(address, 0x1000); + recorder.OnComplete([address, landed, value, original = std::move(original)] { + const std::vector current(0x1000, value); + AgcDriver::GuestMemory::WriteChanged(address, current, original); + *landed = true; + }); + if (submitted) recorder.Submit(); + const auto next = lookup(); + Require(*landed, "the partially resident lookup returned before its pending copied store completed"); + Require(next != previous, "the partially resident lookup reused the snapshot from before its pending copied store"); + Require(surface.Data()[0] == std::to_integer(value), "the pending copied store did not update guest memory"); + Require(lookup() == next, "the partially resident lookup did not reuse the snapshot after its pending copied store"); + previous = next; + } + const auto unrelated = std::make_shared(false); + recorder.NotePendingWrite(address + SurfaceBytes, BlockBytes); + recorder.OnComplete([unrelated] { *unrelated = true; }); + Require(lookup() == previous, "an unrelated pending write replaced the partially resident snapshot"); + Require(!*unrelated, "a partially resident lookup waited for an unrelated pending write"); + recorder.Sync(); + Require(*unrelated, "the unrelated pending completion did not run when synchronized"); +} + +} + +int main(int argc, char** argv) { + try { + if (!AgcDriver::GuestMemory::WriteWatched()) { + std::puts("skipped, guest memory has no write watch"); + return Skipped; + } + std::optional device; + try { + device.emplace(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return Skipped; + } + { + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + ReservedSurface surface; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + else Run(context, surface); + ClearCachedTextures(context.device); + } + device.reset(); + std::puts("partially resident snapshot 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/Pixel32ArExport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp new file mode 100644 index 0000000000..daaad752c8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp @@ -0,0 +1,299 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/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 +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t ElementBytes = 4; +constexpr std::size_t RowBytes = std::size_t{Width} * ElementBytes; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float32Info = 0x0710u; +constexpr std::uint32_t Float32AlternateReversedInfo = 0x1f10u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Export32Ar = 3; +constexpr float Destination = 0.75f; + +struct Row { + std::uint32_t enables; + std::array sources; +}; + +constexpr std::array Rows{{ + {0x3u, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xbu, {0.5f, 0.25f, 2.0f, 0.0625f}}, + {0xfu, {0.125f, 0.5f, 2.0f, 0.0625f}}, + {0x3u, {0.25f, 0.75f, 2.0f, 0.0625f}}, +}}; +constexpr std::uint32_t Height = Rows.size() + 1; +constexpr std::size_t TargetBytes = RowBytes * Height; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Rows.size()> Programs{}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::size_t index) { + auto& code = Programs[index]; + const auto& row = Rows[index]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(row.sources[channel]); + } + code[at++] = 0xf8001800u | row.enables; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + return AgcDriver::Graphics::DecodeColorBuffer(cx, 0); +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::uint32_t waveSize, std::size_t row) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, static_cast(row)}, {Width, 1}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto pixelCode = Program(row); + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, waveSize == 32u ? 0x8000u : 0u}, {0x1c5, Export32Ar}, {0x203, 0u}}; + const auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.targetOutputMode[0] == Export32Ar && pixel.wave32 == (waveSize == 32u), "the pixel registers did not decode as a 32_AR MRT0 export"); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::StorageTexture::FlushPending(address, bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(bytes / sizeof(std::uint32_t)); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::string Describe(const Row& row) { + char text[96]; + std::snprintf(text, sizeof(text), "EN 0x%x of (%g, %g, %g, %g)", row.enables, row.sources[0], row.sources[1], row.sources[2], row.sources[3]); + return text; +} + +struct TargetCase { + const char* name; + std::uint32_t info; + std::uint8_t mapping; + bool blended; +}; + +void ExportTests(AgcDriver::VulkanDevice& device, const TargetCase& test, std::uint32_t waveSize) { + Block block(BlockBytes); + std::vector filled(BlockBytes / sizeof(std::uint32_t), std::bit_cast(Destination)); + std::memcpy(block.data, filled.data(), BlockBytes); + const auto color = DecodeTarget(block, test.info); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.componentMapping == test.mapping && color.elementBytes == ElementBytes, std::string("the ") + test.name + " target did not decode as R32_SFLOAT with component mapping " + Hex(test.mapping)); + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, ElementBytes).Bytes() == TargetBytes, "the linear COLOR_32 target is padded"); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + if (test.blended) { + blend.blendEnable = VK_TRUE; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorBlendOp = VK_BLEND_OP_ADD; + blend.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blend.alphaBlendOp = VK_BLEND_OP_ADD; + } + for (std::size_t row = 0; row < Rows.size(); ++row) Draw(device, color, blend, waveSize, row); + const auto stored = ReadBack(device, block.Address(), TargetBytes); + for (std::size_t row = 0; row < Height; ++row) { + float expected = Destination; + if (row < Rows.size()) { + const auto red = Rows[row].sources[0]; + const auto alpha = Rows[row].sources[1]; + expected = test.blended ? red * alpha + Destination * (1.0f - alpha) : alpha; + } + for (std::uint32_t x = 0; x < Width; ++x) { + const auto actual = stored[row * Width + x]; + if (actual == std::bit_cast(expected)) continue; + const auto source = row < Rows.size() ? "the 32_AR export with " + Describe(Rows[row]) : std::string("a row outside the drawn rows"); + throw std::runtime_error("wave" + std::to_string(waveSize) + " " + source + " into the " + test.name + " target stored " + Hex(actual) + " at pixel (" + std::to_string(x) + ", " + std::to_string(row) + "), expected " + Hex(std::bit_cast(expected))); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + constexpr std::array targets{{ + {"COLOR_32 FLOAT ALT_REV (alpha only)", Float32AlternateReversedInfo, 0xe7u, false}, + {"COLOR_32 FLOAT STD blended with SRC_ALPHA and ONE_MINUS_SRC_ALPHA", Float32Info, 0xe4u, true}, + }}; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const auto& test : targets) ExportTests(*device, test, waveSize); + } + std::puts("32_AR export 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/PixelLdsReadModifyWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp new file mode 100644 index 0000000000..cf2496bda8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsReadModifyWrite.cpp @@ -0,0 +1,198 @@ +#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 Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Factor = 0x00a5c3e1u; +constexpr std::uint32_t Counter = 0x1234u; +constexpr std::uint32_t Slots = 64; +constexpr std::uint32_t CounterOffset = Slots * 16u; +alignas(256) std::array Pixels{}; +alignas(256) std::array Outputs{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +std::vector PixelCode() { + std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; + const auto vop2 = [&](std::uint32_t opcode, std::uint32_t destination, std::uint32_t source, std::uint32_t vector) { + code.push_back(opcode | (destination << 17u) | (vector << 9u) | source); + }; + const auto ds = [&](std::uint32_t opcode, std::uint32_t offset, std::uint32_t address, std::uint32_t data0, std::uint32_t data1, std::uint32_t destination) { + code.push_back(0xd8000000u | (opcode << 18u) | offset); + code.push_back(address | (data0 << 8u) | (data1 << 16u) | (destination << 24u)); + }; + vop2(0x36000000, 8, 0xff, 6); + code.push_back(Slots - 1u); + vop2(0x34000000, 8, 0x84, 8); + code.push_back(0x7e126f06); + vop2(0x16000000, 10, 0xff, 6); + code.push_back(Factor); + code.insert(code.end(), {0x7e1c02c1, 0x7e2002ff, 0xffff0000, 0x7e240280, 0x7e2602ff, Counter, 0xbefc03ff, 0x0000ffff}); + for (std::uint32_t offset = 0; offset < 16; offset += 4) ds(0x0d, offset, 8, 9, 0, 0); + ds(0x1e, 0, 8, 10, 0, 0); + ds(0x1e, 2, 8, 6, 0, 0); + ds(0xa1, 6, 8, 10, 0, 0); + ds(0x1f, 8, 8, 6, 0, 0); + ds(0xa0, 11, 8, 10, 0, 0); + ds(0x20, 12, 8, 10, 0, 12); + ds(0x23, 12, 8, 14, 0, 13); + ds(0x2c, 12, 8, 16, 6, 15); + ds(0x0d, CounterOffset, 18, 19, 0, 0); + ds(0x3e, CounterOffset, 0, 0, 0, 17); + code.push_back(0xbf8cc07f); + for (std::uint32_t dword = 0; dword < 4; ++dword) ds(0x36, dword * 4u, 8, 0, 0, 20 + dword); + code.push_back(0xbf8cc07f); + const std::array stored{20, 21, 22, 23, 12, 13, 15, 17}; + for (std::uint32_t slot = 0; slot < Results; ++slot) { + code.push_back(0xe0702000 | (slot * 4u)); + code.push_back(0x80010006 | (stored[slot] << 8u)); + } + code.insert(code.end(), {0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}); + return code; +} + +std::array Expected(std::uint32_t pixel) { + const auto initial = ~pixel; + const auto product = static_cast(std::uint64_t{Factor} * pixel); + const auto added = initial + product; + const auto incremented = added == 0xffffffffu ? 0u : added + 1u; + const auto masked = (incremented & 0x0000ffffu) | pixel; + return { + (initial & 0xff00ff00u) | (product & 0xffu) | ((pixel & 0xffu) << 16u), + (initial & 0x0000ffffu) | (product & 0xffff0000u), + (initial & 0x00ff0000u) | (pixel & 0xffffu) | (((product >> 16u) & 0xffu) << 24u), + masked, + initial, + added, + incremented, + Counter, + }; +} + +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}; +} + +constexpr std::array, 3> Triangles{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { + const auto& triangles = Triangles; + const auto pixelCode = PixelCode(); + Pixels.fill(std::byte{0}); + Outputs.fill(Sentinel); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 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{ + {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.wave32 = waveSize == 32u; + 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); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Outputs.data(), Results * 4u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + 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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize) { + static constexpr std::array names{"ds_write_b8 dword", "ds_write_b16_d16_hi dword", "ds_write_b16 and ds_write_b8_d16_hi dword", "atomic dword", "ds_add_rtn_u32", "ds_inc_rtn_u32", "ds_mskor_rtn_b32", "ds_append"}; + const auto what = "pixel LDS read-modify-write wave" + std::to_string(waveSize); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Pixels[pixel * 4u + channel] == std::byte{255}, what + ": missing color at pixel " + std::to_string(pixel)); + } + const auto expected = Expected(pixel); + for (std::uint32_t slot = 0; slot < Results; ++slot) { + const auto actual = Outputs[pixel * Results + slot]; + Require(actual == expected[slot], what + ": pixel " + std::to_string(pixel) + " " + names[slot] + " expected " + std::to_string(expected[slot]) + " got " + std::to_string(actual)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto waveSize : {64u, 32u}) { + std::fprintf(stderr, "pixel LDS read-modify-write wave%u\n", waveSize); + for (std::uint32_t pass = 0; pass < 2; ++pass) { + Draw(*device, waveSize); + Check(waveSize); + } + } + std::puts("pixel LDS read-modify-write tests passed: 4 draws, 2048 pixels, 16384 results"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index d1b66658cc..c4fb6eba1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -389,6 +389,12 @@ void SharedCodeHeaders() { Shader copy = vertex.shader; copy.user_data = nullptr; AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + auto moved = vertex.registers; + copy.sh_registers = moved.data(); + AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); + moved[2].value ^= 2u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); + copy.sh_registers = vertex.shader.sh_registers; copy.target ^= 1u; ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, primitive); }, "replaced shader header"); std::vector commands; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp new file mode 100644 index 0000000000..607faa3dd3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SameQueueLabelWait.cpp @@ -0,0 +1,164 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#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 + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array SpinCode{0xf4080200u, 0xfa000000u, 0xbe840381u, 0x9304ff04u, 0x0019660du, 0x8004ff04u, 0x3c6ef35fu, 0x80828102u, 0xbf078002u, 0xbf85fff9u, 0xbf8cc07fu, 0x7e000204u, 0xe0700000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t SpinIterations = 1u << 20u; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Waits = 16; + +struct Slot { + alignas(256) std::array spinOutput{}; + alignas(256) std::array spinDescriptor{}; + alignas(256) volatile std::uint32_t label = 0; + alignas(256) volatile std::uint32_t done = 0; +}; + +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, "same-queue label wait: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + 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; + + Slot& Data() { return *reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +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); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Dispatch(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +void ReleaseLabel(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), Low(target), High(target), value, 0u, 0u}); +} + +void WriteData(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(target), High(target), value}); +} + +void WaitEqual(std::vector& words, volatile std::uint32_t* target, std::uint32_t value) { + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(target), High(target), value, 0xffffffffu, 0x19u}); +} + +std::uint64_t Run(Slot& slot, std::uint32_t value, bool wait) { + slot.spinDescriptor = {Low(slot.spinOutput.data()), High(slot.spinOutput.data()) & 0xffffu, sizeof(slot.spinOutput), 0x31016facu}; + slot.done = 0; + std::vector words; + Dispatch(words, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterations}); + for (std::uint32_t index = 0; index < Waits; ++index) { + ReleaseLabel(words, &slot.label, value + index); + if (wait) WaitEqual(words, &slot.label, value + index); + } + WriteData(words, &slot.done, 1); + const auto before = AgcDriver::GuestMemory::CollectEpochBumps(); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + const auto start = std::chrono::steady_clock::now(); + while (slot.done == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), "the submission did not finish"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + AgcDriverWaitIdle_nid_postfix(); + Require(slot.label == value + Waits - 1, "the last label did not reach memory"); + return AgcDriver::GuestMemory::CollectEpochBumps() - before; +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestBlock block; + auto& slot = block.Data(); + Run(slot, 1, false); + std::uint64_t extra = ~std::uint64_t{0}; + for (std::uint32_t attempt = 0; attempt < 3; ++attempt) { + const auto without = Run(slot, 100 + attempt * 2 * Waits, false); + const auto with = Run(slot, 100 + (attempt * 2 + 1) * Waits, true); + std::cout << "attempt " << attempt << ": " << with << " bumps with " << Waits << " waits, " << without << " without\n"; + extra = std::min(extra, with > without ? with - without : 0); + } + Require(extra < Waits / 2, "WAIT_REG_MEMs satisfied by labels their own queue released earlier in the submission started new collect epochs: " + std::to_string(extra) + " extra bumps for " + std::to_string(Waits) + " waits"); + std::cout << "same-queue label wait test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp new file mode 100644 index 0000000000..4468d5d220 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SdwaCvtF32Ubyte.cpp @@ -0,0 +1,104 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array SdwaCode{ + 0x34020082, 0x34060082, 0xe0302000, 0x80000401, + 0x7e1402ff, 0xabcd1234, 0x7e1602ff, 0xabcd1234, 0x7e1802ff, 0xabcd1234, 0x7e1a02ff, 0xabcd1234, + 0xbf8c3f70, 0x7e1424f9, 0x00040604, 0x7e1624f9, 0x00050604, 0x7e1826f9, 0x000d0604, 0x7e1a28f9, 0x000b0604, + 0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03, + 0xbf810000, +}; + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * Inputs] = tid | ((tid + 64u) << 8u) | ((255u - tid) << 16u) | (((tid * 3u + 128u) & 0xffu) << 24u); + } +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FloatBits(std::uint32_t value) { + return std::bit_cast(static_cast(value)); +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(SdwaCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto source = Input[tid * Inputs]; + const auto* out = &Output[tid * Results]; + const auto where = [&](std::uint32_t result) { return "sdwa cvt f32 ubyte: thread " + std::to_string(tid) + " result " + std::to_string(result) + " is " + Hex(out[result]); }; + const auto byte1 = FloatBits((source >> 8u) & 0xffu); + const auto byte3 = FloatBits(source >> 24u); + const auto signFill = FloatBits((source & 0x80000000u) != 0u ? 0xffu : 0u); + Require(out[0] == byte1, where(0) + ", expected " + Hex(byte1) + ": v_cvt_f32_ubyte1 src0_sel:WORD_0 must convert bits 15:8"); + Require(out[1] == byte3, where(1) + ", expected " + Hex(byte3) + ": v_cvt_f32_ubyte1 src0_sel:WORD_1 must convert bits 31:24"); + Require(out[2] == signFill, where(2) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte2 src0_sel:WORD_1 src0_sext must convert the sign extension of the high word"); + Require(out[3] == signFill, where(3) + ", expected " + Hex(signFill) + ": v_cvt_f32_ubyte3 src0_sel:BYTE_3 src0_sext must convert the sign extension of the top byte"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device); + Check(); + std::puts("sdwa cvt f32 ubyte 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/SkipShader.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp new file mode 100644 index 0000000000..377e819b46 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SkipShader.cpp @@ -0,0 +1,71 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Skipped{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +alignas(256) constexpr std::array Kept{0xd7460000u, 0x04010c04u, 0x7e0202ffu, 0x12345678u, 0xe0702000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Stored = 0x12345678u; +alignas(256) std::array SkippedOutput{}; +alignas(256) std::array KeptOutput{}; + +void Dispatch(const std::array& code, std::array& output) { + const auto address = reinterpret_cast(output.data()); + const std::array data{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), static_cast(output.size()), 0x00014204u}; + const auto program = reinterpret_cast(code.data()); + std::vector words{ + 0xc0037600u, 0x207, 64, 1, 1, + 0xc0027600u, 0x20c, static_cast(program >> 8u), static_cast(program >> 40u), + 0xc0017600u, 0x213, (4u << 1u) | (1u << 7u), + 0xc0047600u, 0x240, data[0], data[1], data[2], data[3], + 0xc0031500u, 1, 1, 1, 0x41u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(address, sizeof(output)); +} + +} + +int main() { + try { + char value[32]; + std::snprintf(value, sizeof(value), "%llx", static_cast(reinterpret_cast(Skipped.data()))); +#ifdef _WIN32 + Require(_putenv_s("APS5_SKIP_SHADER", value) == 0, "cannot set APS5_SKIP_SHADER"); +#else + Require(setenv("APS5_SKIP_SHADER", value, 1) == 0, "cannot set APS5_SKIP_SHADER"); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + SkippedOutput.fill(Untouched); + KeptOutput.fill(Untouched); + Dispatch(Skipped, SkippedOutput); + Dispatch(Kept, KeptOutput); + for (std::size_t i = 0; i < SkippedOutput.size(); ++i) { + Require(SkippedOutput[i] == Untouched, "the dispatch of the skipped shader stored dword " + std::to_string(i)); + Require(KeptOutput[i] == Stored, "the dispatch of another shader did not store dword " + std::to_string(i)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("skip shader 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/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp new file mode 100644 index 0000000000..6c271a9f48 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#ifndef _WIN32 +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +int main() { + std::printf("skipped, udmabuf imports are Linux only\n"); + return VulkanTestSkipped; +} +#else +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; + +std::uint8_t* mapping = nullptr; +std::uint64_t mappingBytes = 0; +int backing = -1; + +bool Resolve(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + const auto begin = reinterpret_cast(mapping); + if (address < begin || address + bytes > begin + mappingBytes) return false; + *file = dup(backing); + *offset = address - begin; + return *file >= 0; +} + +std::uint64_t UdmabufLimitBytes() { + std::ifstream limit("/sys/module/udmabuf/parameters/size_limit_mb"); + std::uint64_t megabytes = 64; + limit >> megabytes; + return megabytes << 20u; +} + +bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device; + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); +} + +} + +int main() { + try { + const int udmabuf = open("/dev/udmabuf", O_RDWR | O_CLOEXEC); + if (udmabuf < 0) { + std::printf("skipped, host imports are unavailable: /dev/udmabuf is not accessible\n"); + return VulkanTestSkipped; + } + close(udmabuf); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + + constexpr std::size_t ProbeBytes = 65536; + auto* probe = static_cast(std::aligned_alloc(ProbeBytes, ProbeBytes)); + Require(probe != nullptr, "sparse import: cannot allocate the probe"); + GuestAllocations::Mutation().Add(probe, ProbeBytes, true, true, true); + bool adopted = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{1u, 2u, 3u, 4u}; + adopted = device->FillBuffer(reinterpret_cast(probe), ProbeBytes, pattern); + device->WaitIdle(); + } + if (!adopted) { + std::printf("skipped, the device does not import guest memory\n"); + return VulkanTestSkipped; + } + + const auto bytes = (UdmabufLimitBytes() / ChunkBytes + 2) * ChunkBytes; + mappingBytes = bytes + ChunkBytes; + backing = memfd_create("anyps5-sparse-import-test", MFD_CLOEXEC | MFD_ALLOW_SEALING); + Require(backing >= 0 && ftruncate(backing, static_cast(mappingBytes)) == 0, "sparse import: cannot make the backing file"); + Require(fcntl(backing, F_ADD_SEALS, F_SEAL_SHRINK) == 0, "sparse import: cannot seal the backing file"); + void* mapped = mmap(nullptr, mappingBytes, PROT_READ | PROT_WRITE, MAP_SHARED, backing, 0); + Require(mapped != MAP_FAILED, "sparse import: cannot map the backing file"); + mapping = static_cast(mapped); + GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); + + const auto Register = [](std::uint8_t* memory, std::uint64_t size) { + GuestAllocations::Mutation().Add(memory, static_cast(size), true, true, true); + GuestAllocations::Mapped ranges; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == reinterpret_cast(memory)) ranges.push_back(range); + } + Require(ranges.size() == 1, "sparse import: the allocation is not registered"); + return ranges; + }; + + auto* small = mapping + bytes; + constexpr std::uint64_t SmallBytes = std::uint64_t{1} << 20u; + device->ImportGuestMemory(Register(small, SmallBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + const bool smallImported = Imported(device->Device(), reinterpret_cast(small), static_cast(SmallBytes)); + GuestAllocations::Mutation().Remove(small); + if (!smallImported) { + std::printf("skipped, the device does not import dma-buf memory\n"); + return VulkanTestSkipped; + } + + const auto boundary = ChunkBytes; + std::memset(mapping + boundary - 65536, 0x11, 131072); + const auto ranges = Register(mapping, bytes); + const auto address = reinterpret_cast(mapping); + const bool atMap = device->ImportGuestMemory(ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + Require(Imported(device->Device(), address, static_cast(bytes)), "sparse import: a range larger than udmabuf's size limit was not imported"); + Require(atMap, "sparse import: the import made without the GPU lock was refused"); + + bool filled = false; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du, 0x5EEDF00Du}; + filled = device->FillBuffer(address + boundary - 32768, 65536, pattern); + device->WaitIdle(); + } + Require(filled, "sparse import: the fill across the chunk boundary was not served by the import"); + for (std::uint64_t offset = boundary - 32768; offset < boundary + 32768; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, mapping + offset, sizeof(value)); + Require(value == 0x5EEDF00Du, "sparse import: the GPU fill did not reach the backing memory across the chunk boundary"); + } + Require(mapping[boundary - 32769] == 0x11 && mapping[boundary + 32768] == 0x11, "sparse import: the GPU fill reached past its range"); + + GuestAllocations::Mutation().Remove(mapping); + GuestAllocations::Mutation().Remove(probe); + std::free(probe); + std::cout << "AGC sparse dma-buf import tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3caabc9f92..99e31a350a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -57,12 +57,12 @@ std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } -std::array TextureDescriptor(const std::uint32_t* texels) { +std::array TextureDescriptor(const std::uint32_t* texels, std::uint32_t side = Side) { const auto address = AddressOf(texels); 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, @@ -91,9 +91,9 @@ void Dispatch(std::span code, std::uint32_t threads, std::u AgcDriver::Submit(&packet, 0); } -void Store(const std::uint32_t* texels) { +void Store(const std::uint32_t* texels, std::uint32_t side = Side) { std::array userData{}; - const auto texture = TextureDescriptor(texels); + const auto texture = TextureDescriptor(texels, side); std::copy(texture.begin(), texture.end(), userData.begin() + 8); Dispatch(StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); } @@ -205,7 +205,11 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch }; +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied }; + +// One 64 KiB block of the surface: the image of its last block, and the source copied into it. +constexpr std::uint32_t BlockSide = 128; +constexpr std::size_t LastBlock = SurfaceBytes - Block; struct Case { const char* name; @@ -217,12 +221,12 @@ enum class Outcome { Kept, Lost, Missed }; Outcome Run(const Case& test) { std::optional source; - if (test.kind == Kind::Copied) source.emplace(); - Surface surface(test.kind == Kind::LeftWatch); + if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); + const bool leaves = test.kind == Kind::LeftWatch || test.kind == Kind::WrittenThenLeftWatch || test.kind == Kind::ImportLeftWatch; + Surface surface(leaves); auto* bytes = surface.Bytes(); const auto address = surface.Address(); std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); - bool unsaved = false; if (test.kind == Kind::Copied) { Store(source->Texels()); Settle(hold, *source, Stored); @@ -230,8 +234,12 @@ Outcome Run(const Case& test) { Settle(hold, *source, Filled); } Store(surface.Texels()); - if (test.kind == Kind::Filled) { + if (test.kind == Kind::Filled || leaves) { Settle(hold, surface, Stored); + if (leaves) { + std::vector settled(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, settled); + } Fill(surface.Texels()); if (!HoldPending(hold, surface, Filled)) return Outcome::Missed; } else if (test.kind == Kind::Copied) { @@ -246,10 +254,58 @@ Outcome Run(const Case& test) { std::fprintf(stderr, "[store over untracked] %s: not run, the surface has no host import\n", test.name); return Outcome::Kept; } + } else if (test.kind == Kind::NeighbourCopied) { + // The surface's image has results pending in its first block when a copy replaces the + // image of its last block: the copy supersedes the saved bytes there only. + const auto settledLive = [&](std::uint64_t at, std::size_t size) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + StorageTexture::FlushAllPending("store over untracked fixture"); + const bool live = StorageTexture::FindLive(at, size) != nullptr; + hold.unlock(); + if (live) return; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the block's storage image was never created"); + std::this_thread::yield(); + } + }; + Store(surface.Texels() + LastBlock / 4u, BlockSide); + settledLive(address + LastBlock, Block); + Store(source->Texels(), BlockSide); + settledLive(source->Address(), Block); + std::fill(surface.Texels(), surface.Texels() + SurfaceBytes / 4u, Untouched); + Store(surface.Texels()); + // Pending results are the surface image's alone once it exists. + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr) { + if (StorageTexture::AnyPending()) break; + hold.unlock(); + return Outcome::Missed; + } + hold.unlock(); + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's storage image was never created"); + std::this_thread::yield(); + } + const auto from = StorageTexture::FindLive(source->Address(), Block); + const auto to = StorageTexture::FindLive(address + LastBlock, Block); + Require(from != nullptr && to != nullptr, "store over untracked: the blocks have no storage images to copy between"); + const char* refusal = nullptr; + if (!to->CopyFrom(*from, refusal)) { + Require(refusal != nullptr && std::string_view(refusal) == "not imported", "store over untracked: the image copy was refused"); + StorageTexture::FlushAllPending("store over untracked fixture"); + std::fprintf(stderr, "[store over untracked] %s: not run, the block has no host import\n", test.name); + return Outcome::Kept; + } } else if (!HoldPending(hold, surface, Stored)) { return Outcome::Missed; } - if (test.kind == Kind::LeftWatch) { + const auto writePattern = [&] { + for (std::size_t offset = 0; offset < Page; ++offset) static_cast(bytes)[offset] = Pattern(offset); + }; + if (test.kind == Kind::WrittenThenLeftWatch) writePattern(); + if (leaves) { if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { StorageTexture::FlushAllPending("store over untracked fixture"); std::fprintf(stderr, "[store over untracked] %s: not run, the surface is not write-watched\n", test.name); @@ -257,32 +313,21 @@ Outcome Run(const Case& test) { } const auto image = StorageTexture::FindLive(address, SurfaceBytes); Require(image != nullptr, "store over untracked: the surface has no storage image"); - unsaved = !image->GuestSnapshotValid(); - AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!image->GuestSnapshotValid(), "store over untracked: the pending result had saved guest bytes while its memory was write-watched"); + if (test.kind == Kind::ImportLeftWatch) Require(AgcDriver::GuestMemory::ImportWatched(address, SurfaceBytes, [] { return true; }, false), "store over untracked: the import that leaves the write watch failed"); + else AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "store over untracked: the surface is still write-watched"); } std::array read{}; if (test.kind == Kind::ReadBefore) AgcDriver::GuestMemory::Read(address + Block + Page, read); - for (std::size_t offset = 0; offset < Page; ++offset) static_cast(bytes)[offset] = Pattern(offset); - if (unsaved) { - bool refused = false; - try { - AgcDriver::GuestMemory::Read(address + test.access, read); - } catch (const std::runtime_error& error) { - if (std::string_view(error.what()).find("outside the write watch") == std::string_view::npos) throw; - refused = true; - } - static_cast(StorageTexture::DiscardPendingInside(address, SurfaceBytes)); - if (refused && LostBytes(bytes) == 0) return Outcome::Kept; - std::fprintf(stderr, "[store over untracked] %s: the result was stored with neither write stamps nor saved bytes to tell what the CPU wrote\n", test.name); - return Outcome::Lost; - } + if (test.kind != Kind::WrittenThenLeftWatch) writePattern(); AgcDriver::GuestMemory::Read(address + test.access, read); const auto lostAtAccess = LostBytes(bytes); StorageTexture::FlushAllPending("store over untracked fixture"); std::vector whole(SurfaceBytes); AgcDriver::GuestMemory::Read(address, whole); const auto lostAtEnd = LostBytes(bytes); - if (test.kind == Kind::Filled || test.kind == Kind::Copied) Require(static_cast(surface.Texels())[2 * Block / 4u] == Filled, "store over untracked: the result did not reach a block the CPU left alone"); + if (test.kind == Kind::Filled || test.kind == Kind::Copied || leaves) Require(static_cast(surface.Texels())[2 * Block / 4u] == Filled, "store over untracked: the result did not reach a block the CPU left alone"); if (lostAtAccess == 0 && lostAtEnd == 0) return Outcome::Kept; std::fprintf(stderr, "[store over untracked] %s: of 0x%zx bytes the CPU wrote while the result was pending, %zu were lost after the access at +0x%zx and %zu after every result was stored\n", test.name, Page, lostAtAccess, test.access, lostAtEnd); return Outcome::Lost; @@ -303,12 +348,15 @@ int main() { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, {"an image copy into the surface", Kind::Copied, 0xe000}, {"a surface that left the write watch", Kind::LeftWatch, 0xe000}, + {"a surface written before it left the write watch", Kind::WrittenThenLeftWatch, 0xe000}, + {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, + {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 925da125e9..c016c7423a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -1,16 +1,27 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include #include +#include #include #include +#include #include #include #include #include +#include #include namespace { @@ -21,7 +32,9 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Width = 256; constexpr std::uint32_t Height = 80; constexpr std::array Background{16, 24, 40, 255}; +constexpr std::size_t GuestBytes = 65536; alignas(256) std::array Pixels{}; +std::array HullUserData{0x1000u}; alignas(256) constexpr std::array LocalCode{ 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, @@ -40,6 +53,36 @@ alignas(256) constexpr std::array HullCode{ 0xbf810000, }; +alignas(256) constexpr std::array HullCompareBranch{ + 0xd4040016, 0x00020080, 0xbeea0416, 0xbf870001, 0xbf800000, 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, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullSingleInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullSingleOutput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x022224ff, 0x00000020, 0xdbfc0000, + 0x02000015, 0x7e2c0280, 0xd5430016, 0x045a24ff, 0x00000010, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0xbf800000, 0xbe94047e, 0xb06a0020, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc0201, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + alignas(256) constexpr std::array HullRestoredExecSaved{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -50,6 +93,23 @@ alignas(256) constexpr std::array HullRestoredExecSaved{ 0xe0741000, 0x0202070b, 0xbf810000, }; +alignas(256) constexpr std::array LocalCodeStride24{ + 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, + 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, + 0x3e1008ff, 0x3e800000, 0xd54b0009, 0x03c5e105, 0x16140698, 0xd9340000, 0x0000080a, 0xd8340008, + 0x0000070a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, +}; + +alignas(256) constexpr std::array HullNextInput{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x16002898, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0018, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xbe94047e, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, + 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array DomainCode{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, @@ -57,6 +117,22 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainRelativeData{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0x4a2a0e81, 0x7e2a0d15, 0x102a2aff, 0x3e000000, + 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, + 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, + 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainCompareBranch{ + 0xf8000941, 0x00000000, 0xd4040014, 0x00020a80, 0xbeea0414, 0xbf870001, 0xbf800000, 0x34020e84, + 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 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 LocalIndexAsData{ 0xd5480004, 0x02090502, 0xd5480005, 0x02090102, 0xd5480006, 0x02110902, 0xd5480007, 0x02111102, 0x7e080d04, 0x7e0a0d05, 0x7e0c0d06, 0x7e0e0d07, 0x10100cff, 0x3f19999a, 0x061010ff, 0xbf666666, @@ -84,6 +160,16 @@ alignas(256) constexpr std::array HullPatchConstant{ 0x0202070b, 0xe0701064, 0x0202060b, 0xbf810000, }; +alignas(256) constexpr std::array HullRelativeData{ + 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, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, + 0xe0781000, 0x0403180a, 0x4a162481, 0x161616ff, 0x000000d0, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0x4a182481, 0x7e180d0c, 0xe0701064, 0x02020c0b, 0xbf810000, +}; + alignas(256) constexpr std::array HullUnguardedFactors{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -132,6 +218,47 @@ alignas(256) constexpr std::array HullAndSaveexec{ 0xbf810000, }; +alignas(256) constexpr std::array HullSaveexecLoop{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xbe980382, 0xbe9a0480, + 0x7d882881, 0xbe96246a, 0x889a7e1a, 0xbefe0416, 0x80988118, 0xbf078018, 0xbf85fff9, 0xe0781000, + 0x02020216, 0x7d882881, 0xbe94246a, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300306, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, + 0xbf810000, +}; + +alignas(256) constexpr std::array HullLaneCount{ + 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, 0xbe98107e, 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, + 0xd8dc1008, 0x07000013, 0xbf8cc07f, 0x7e300218, 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, + 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, + 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullBdaScale{ + 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, 0xd8d80008, 0x06000013, 0xd8dc1008, + 0x07000013, 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, + 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + +alignas(256) constexpr std::array HullBdaScaleWithoutBarrier{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, + 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, 0x02000015, + 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000030, 0xbf8cc07f, 0xe0781000, 0x02020216, 0x7da82881, + 0xbe94047e, 0xb06a0060, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xd8dc1008, 0x07000013, + 0xbf8cc07f, 0x7e180280, 0xdc308000, 0x0d06000c, 0xbf8c3f70, 0x100c1b06, 0x7e300306, 0x7e320306, + 0x7e340306, 0x7e360306, 0x34142484, 0xbefe0414, 0xe0781000, 0x0403180a, 0x4a162481, 0x16161690, + 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, +}; + alignas(256) constexpr std::array DomainPatchRead{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, @@ -210,6 +337,17 @@ alignas(256) constexpr std::array Indices{ Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), }; +alignas(256) constexpr std::array LatePatchIndices{ + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), + Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), +}; + alignas(256) constexpr std::array QuadIndices{ Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 2, 2), Corner(0, 0, 2), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 2, 2), Corner(1, 0, 2), @@ -223,7 +361,14 @@ struct PatchDraw { float maxLevel = 64.0f; }; +alignas(256) constexpr std::array NextInputIndices{ + Corner(0, 0, 0), Corner(0, 0, 0), Corner(0, 2, 0), Corner(0, 1, 2), + Corner(1, 0, 0), Corner(1, 0, 0), Corner(1, 2, 0), Corner(1, 1, 2), + Corner(2, 0, 0), Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 1, 2), +}; + const PatchDraw Triangles{Patches, Indices}; +const PatchDraw NextInputTriangles{{4u, 3u, 1u, 2u, 2u}, NextInputIndices}; struct Stages { ShaderRecompiler::RecompileResult local; @@ -233,11 +378,10 @@ struct Stages { }; ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor, const PatchDraw& patches = Triangles) { - static const std::array hullUserData{0x1000u}; static const std::array domainUserData{}; const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const bool hull = stage == ShaderStage::TessellationControl; - const auto userData = hull ? std::span(hullUserData) : stage == ShaderStage::TessellationEvaluation ? std::span(domainUserData) : std::span(); + const auto userData = hull ? std::span(HullUserData) : stage == ShaderStage::TessellationEvaluation ? std::span(domainUserData) : std::span(); std::optional pixel; std::optional vertex; std::optional graphics; @@ -277,6 +421,12 @@ void ExpectRefusal(const ShaderRecompiler::SpirvTarget& target, ShaderStage stag Require(false, "a tessellation program was accepted although " + reason); } +void RequireCompareBranches(const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + Require(!Compile(target, ShaderStage::TessellationEvaluation, DomainCompareBranch, cursor).spirv.empty(), "a domain program branching on a compare copied to VCC was not compiled"); + Require(!Compile(target, ShaderStage::TessellationControl, HullCompareBranch, cursor).spirv.empty(), "a hull program branching on a compare copied to VCC was not compiled"); +} + std::array PixelAt(std::uint32_t x, std::uint32_t y) { const auto offset = (static_cast(y) * Width + x) * 4u; return {std::to_integer(Pixels[offset]), std::to_integer(Pixels[offset + 1]), std::to_integer(Pixels[offset + 2]), std::to_integer(Pixels[offset + 3])}; @@ -344,10 +494,10 @@ 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) { +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::uint32_t cursor = 0; const Stages stages{ - Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), @@ -384,9 +534,29 @@ void RequireCounterClockwise(AgcDriver::VulkanDevice& device, const ShaderRecomp Require(CoveredPixels(0, false) > 100 && CoveredPixels(2, false) > 100, "counter-clockwise output did not reverse the control points' winding"); } +bool HasInstruction(std::span words, spv::Op opcode, std::uint32_t length, std::uint32_t operand, std::uint32_t value) { + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && cursor + count <= words.size(), "malformed SPIR-V while scanning a tessellation module"); + if ((words[cursor] & 0xffffu) == opcode && count == length && words[cursor + operand] == value) return true; + cursor += count; + } + return false; +} + +bool HasSpacing(const ShaderRecompiler::RecompileResult& domain, spv::ExecutionMode spacing) { + return HasInstruction(domain.spirv.Words(), spv::OpExecutionMode, 3u, 2u, static_cast(spacing)); +} + +bool HasLevelBound(const ShaderRecompiler::RecompileResult& hull, float bound) { + return HasInstruction(hull.spirv.Words(), spv::OpConstant, 4u, 3u, std::bit_cast(bound)); +} + void RequireLevelClamps(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(-0.5f + 1.0f / 3.0f); const auto stages = CompileTriangles(target, Triangles); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual), "fractional-odd partitioning did not compile to fractional-odd spacing"); + Require(HasLevelBound(stages.hull, 63.0f) && !HasLevelBound(stages.hull, 64.0f), "fractional-odd partitioning did not bound its tessellation factors at 63"); const PatchDraw flattened{Patches, Indices, 1.0f, 1.0f}; Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, flattened); Require(CoveredPixels(2, false) > 100 && CoveredPixels(2, true) == 0, "VGT_HOS_MAX_TESS_LEVEL 1 did not flatten the level-5 patch, centre " + PixelText(PixelAt(Column(2, 0.25f), center))); @@ -418,6 +588,107 @@ void RequirePatchConstants(AgcDriver::VulkanDevice& device, const ShaderRecompil Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); } +void RequireIntegerPartitioning(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + const PatchDraw patches{{3u, 3u, 1u, 0u, 2u}, Indices}; + const auto stages = CompileTriangles(target, patches); + Require(HasSpacing(stages.domain, spv::ExecutionModeSpacingEqual) && !HasSpacing(stages.domain, spv::ExecutionModeSpacingFractionalOdd), "integer partitioning did not compile to equal spacing"); + Require(HasLevelBound(stages.hull, 64.0f) && !HasLevelBound(stages.hull, 63.0f), "integer partitioning did not bound its tessellation factors at 64"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, "integer partitioning: the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "integer partitioning: the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "integer partitioning: the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); +} + +void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [hull, domain, name] : {std::tuple{std::span(HullRelativeData), std::span(DomainPatchRead), "hull"}, std::tuple{std::span(HullCode), std::span(DomainRelativeData), "domain"}}) { + for (const auto& [patches, blue] : {std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u, true}, Indices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 2u, 4u, true}, Indices}, 32}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 4u, true}, LatePatchIndices}, 96}, std::pair{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 2u, true}, Indices}, 32}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, 32}, std::pair{2u, blue}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group did not take its blue channel from the " + name + " program's relative patch index, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); + } + } +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, 2 * GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(GuestBytes, 2 * GuestBytes)); +#endif + Require(block != nullptr, "tessellation: cannot allocate the guest block"); + std::memset(block, 0, 2 * GuestBytes); + GuestAllocations::Mutation().Add(block, GuestBytes, 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; } + std::uint64_t Address() const { return reinterpret_cast(block); } + +private: + std::uint8_t* block = nullptr; +}; + +void RequireHullBdaReads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + GuestBlock guest; + const float scale = 1.0f; + std::memcpy(guest.Data(), &scale, sizeof(scale)); + const auto address = [](std::uint64_t value) { + HullUserData[6] = static_cast(value); + HullUserData[7] = static_cast(value >> 32u); + }; + const bool llvmpipe = device.DeviceName().starts_with("llvmpipe"); + for (const auto& [hull, program] : {std::pair{std::span(HullBdaScale), std::string("hull program scaling its levels by a BDA read after s_barrier")}, std::pair{std::span(HullBdaScaleWithoutBarrier), std::string("hull program scaling its levels by a BDA read without s_barrier")}}) { + address(guest.Address()); + RequirePatches(device, target, hull, program); + if (llvmpipe) { + std::puts(("skipped the unmapped read of the " + program + ": llvmpipe runs tessellation-control loads that the check of the unmapped address skips").c_str()); + continue; + } + address(guest.Address() + GuestBytes); + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor), + Compile(target, ShaderStage::TessellationControl, hull, cursor), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor), + Compile(target, ShaderStage::Fragment, PixelCode, cursor), + }; + std::string failure; + try { + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE); + } catch (const std::exception& error) { + failure = error.what(); + } + device.WaitIdle(); + Require(failure.find("BDA access failed") != std::string::npos && failure.find("stage=7 reason=1") != std::string::npos, program + ": the unmapped read was reported as \"" + failure + "\", not as a tessellation-control BDA fault"); + } + address(0u); +} + void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { const auto center = Row(0.0f); std::array frontFacing{}; @@ -490,6 +761,24 @@ void RequireRegisteredStages() { AgcDriverShutdown_nid_postfix(); } +ShaderRecompiler::RecompileResult CompileHull(const ShaderRecompiler::SpirvTarget& target, std::span code, const ShaderRecompiler::TessellationConfiguration& configuration) { + static const std::array userData{0x1000u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::TessellationControl, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + ShaderRecompiler::BindingLayout{0, 0, 0, AgcDriver::Graphics::PipelinePushSlotBytes}, + ShaderRecompiler::GraphicsCompileContext{0u, {}, std::nullopt, configuration, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1}}, + }; + return ShaderRecompiler::Recompile(request); +} + +void RequireSingleControlPointHulls(const ShaderRecompiler::SpirvTarget& target) { + Require(!CompileHull(target, HullSingleInput, {1u, 3u, 1u, 2u, 2u}).spirv.empty(), "a hull that reads only the one input control point of its patch was not compiled"); + 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"); +} + } int main() { @@ -501,15 +790,20 @@ int main() { std::puts("tessellation: skipped, the device has no tessellation shaders"); return VulkanTestSkipped; } + RequireSingleControlPointHulls(target); ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not a whole number of control points"); ExpectRefusal(target, ShaderStage::TessellationControl, HullHalfSlotIndex, "not a whole number of control points"); - ExpectRefusal(target, ShaderStage::TessellationControl, HullAndSaveexec, "lane mask"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullLaneCount, "lane mask"); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourRecord, "more than one patch-constant record", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainFixedPatchRead, "not indexed by its relative patch", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainNeighbourPoint, "neither its patch's control points nor a patch-constant record", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainCulledPrimitive, "passthrough primitive"); ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); + ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -517,12 +811,19 @@ int main() { RequirePatches(*device, target, HullCode, "hull program"); 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, 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}); RequireCounterClockwise(*device, target); RequireLevelClamps(*device, target); + RequireIntegerPartitioning(*device, target); RequireQuads(*device, target); RequirePatchConstants(*device, target); + RequireRelativePatches(*device, target); + RequireHullBdaReads(*device, target); + RequireCompareBranches(target); device.reset(); RequireRegisteredStages(); std::puts("Tessellation tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp index 8576fd86e0..dddb4482e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp @@ -51,7 +51,7 @@ constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 4> Programs{}; +alignas(256) std::array, 6> Programs{}; std::size_t programCount = 0; std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -246,6 +246,8 @@ int main() { Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x12u, 0xbeu, 0x42u, 0x7fu}, "a UINT16_ABGR export into an 8_8_8_8_UINT target"); Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x23u, 0x9au, 0x65u, 0x81u}, "a UINT16_ABGR export into a SW_64KB_D_X target", AgcDriver::Graphics::ColorTileMode::D64KBX); Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x31u, 0x7cu, 0x42u, 0x96u}, "a UINT16_ABGR export into a SW_64KB_D_X target with XOR", AgcDriver::Graphics::ColorTileMode::D64KBX, 0x4600); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x47u, 0x2bu, 0xd1u, 0x6eu}, "a UINT16_ABGR export into a SW_64KB_Z_X target", AgcDriver::Graphics::ColorTileMode::Z64KBX); + Expect(*device, Rgba8UintInfo, VK_FORMAT_R8G8B8A8_UINT, 4, {0x5cu, 0x83u, 0x19u, 0xa4u}, "a UINT16_ABGR export into a SW_64KB_Z_X target with XOR", AgcDriver::Graphics::ColorTileMode::Z64KBX, 0x4600); std::puts("8_8_8_8_UINT color target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index fe7d3b448f..5b5a66615f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -25,6 +25,25 @@ alignas(256) constexpr std::array VertexCode{ 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, }; +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, 0xbf88000c, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80000cf, 0x2f2e2d2c, 0xf80008df, 0x33323130, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +constexpr ShaderRecompiler::MeshConfiguration Subgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + alignas(256) constexpr std::array PixelCode{ 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, }; @@ -78,25 +97,29 @@ bool Expected(Visible visible, std::uint32_t pixel) { return false; } -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row, bool mesh) { Pixels.fill(std::byte{0}); std::array vertices{}; for (std::size_t i = 0; i < vertices.size(); ++i) { vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {row.component0[i], row.component1[i], row.component2[i], 0.0f}}; } - std::vector vertexUserData(4, 0u); + std::vector vertexUserData(mesh ? 12u : 4u, 0u); const auto vertexBuffer = BufferDescriptor(vertices.data(), sizeof(Vertex), 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))}}}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin() + (mesh ? 8 : 0)); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const std::span code = mesh ? std::span(MeshCode) : std::span(VertexCode); + const auto stage = mesh ? ShaderStage::Mesh : ShaderStage::Vertex; + const std::array vertexMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; vertexInfo.paClVsOutCntl = row.control; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {stage, reinterpret_cast(code.data()), code, 0, {}}, {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, device.Target(), - {0, 0, 0, 64} + {0, 0, 0, mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : 64u} }; + if (mesh) vertex.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; vertex.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); @@ -115,12 +138,13 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); const std::array shaders{{ - {ShaderStage::Vertex, &vertexResult, 0}, + {stage, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())} }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + if (mesh) state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, waveSize, waveSize, Subgroup, std::nullopt}; + else state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; state.paClVsOutCntl = row.control; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; @@ -135,13 +159,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro 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(); } -void Check(const Row& row, std::uint32_t waveSize) { - const auto what = std::string("vertex clip and cull distances, ") + row.name + ", wave" + std::to_string(waveSize); +void Check(const Row& row, std::uint32_t waveSize, bool mesh) { + const auto what = std::string(mesh ? "mesh" : "vertex") + " clip and cull distances, " + row.name + ", wave" + std::to_string(waveSize); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { const auto value = std::to_integer(Pixels[pixel * 4u]); Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); @@ -163,10 +186,18 @@ int main() { } for (const auto waveSize : {64u, 32u}) { for (const auto& row : Rows) { - Draw(*device, waveSize, row); - Check(row, waveSize); + Draw(*device, waveSize, row, false); + Check(row, waveSize, false); } } + if (device->Target().mesh.has_value()) { + for (const auto& row : Rows) { + Draw(*device, 64u, row, true); + Check(row, 64u, true); + } + } else { + std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); + } std::puts("vertex clip and cull distance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp new file mode 100644 index 0000000000..df9e6f2c6d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexUserDataTail.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.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 + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::byte Kept{0x40}; +using Image = std::array; +alignas(256) Image Pixels{}; +alignas(256) std::array Prolog{}; +alignas(256) std::array Reference{}; +alignas(256) std::array Continuation{0xbf840003u, 0x7e08020cu, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) const std::array OtherContinuation{0xbf840003u, 0x7e0802f4u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array VertexColorPixelCode{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020202u, 0xbf810000u}; + +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); +} + +Image Draw(const std::uint32_t* code, const std::uint32_t* continuation, std::uint32_t distanceLayout = 0, bool vertexColor = false) { + Pixels.fill(Kept); + const auto vertex = reinterpret_cast(code); + const auto pixel = reinterpret_cast(vertexColor ? VertexColorPixelCode.data() : PixelCode.data()); + const auto next = reinterpret_cast(continuation); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, vertexColor ? 1u : 0u}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, + {0x207, distanceLayout}, {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, 2u << 1u}, + {0x8c, static_cast(next)}, {0x8d, static_cast(next >> 32u)}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + 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}); + const auto color = reinterpret_cast(Pixels.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + Append(commands, 0x2d, {3, 2}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "vertex tail draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +Image Check(std::span prefix, std::span continuation, std::uint32_t distanceLayout = 0, std::size_t continuationWords = 5, bool vertexColor = false) { + Prolog.fill(0xbf9f0000u); + Reference.fill(0xbf9f0000u); + std::copy(prefix.begin(), prefix.end(), Prolog.begin()); + Prolog[prefix.size()] = 0xbe802008u; + std::copy(prefix.begin(), prefix.end(), Reference.begin()); + std::copy_n(continuation.begin(), continuationWords, Reference.begin() + prefix.size()); + const auto expected = Draw(Reference.data(), continuation.data(), distanceLayout, vertexColor); + for (unsigned repeat = 0; repeat < 2u; ++repeat) { + const auto actual = Draw(Prolog.data(), continuation.data(), distanceLayout, vertexColor); + Require(actual == expected, "linked vertex tail pixels differ from the monolithic shader"); + } + return expected; +} + +void CheckBranchColors(const Image& written, const Image& skipped, const char* state) { + std::size_t covered = 0; + for (std::size_t pixel = 0; pixel < written.size(); pixel += 4) { + const bool untouched = written[pixel] == Kept; + for (std::size_t component = 0; component < 4; ++component) { + Require(written[pixel + component] == (untouched ? Kept : std::byte{0xff}) && + skipped[pixel + component] == (untouched ? Kept : std::byte{0}), std::string(state) + " did not select the expected vertex color"); + } + if (!untouched) ++covered; + } + Require(covered != 0 && covered != written.size() / 4, std::string(state) + " reference triangle did not cross the render target"); +} + +void ControlStateDraws(std::vector prefix) { + alignas(256) const std::array sccContinuation{0xbf840001u, 0x7e0c02f2u, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.insert(prefix.end() - 1, 0x7e0c0280u); + const auto trueScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + prefix.back() = 0xbf070c0cu; + const auto falseScc = Check(prefix, sccContinuation, 0, sccContinuation.size(), true); + CheckBranchColors(trueScc, falseScc, "SCC"); + alignas(256) const std::array execContinuation{0x7e0c02f2u, 0xbefe040eu, 0xf800020fu, 0x06060606u, 0xf80008cfu, 0x04030201u, 0xbf810000u}; + prefix.back() = 0xbf060c0cu; + prefix.push_back(0xbe8e047eu); + const auto activeExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + prefix.push_back(0xbefe0480u); + const auto emptyExec = Check(prefix, execContinuation, 0, execContinuation.size(), true); + CheckBranchColors(activeExec, emptyExec, "EXEC"); +} + +void VertexTailDraws() { + std::vector prefix{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xbe8c03ffu, 0x3f800000u, 0xbf060c0cu, + }; + const auto original = Check(prefix, Continuation); + const auto kept = std::count(original.begin(), original.end(), Kept); + Require(kept != 0 && kept != static_cast(original.size()), "reference triangle did not cross the render target"); + const auto changedAddress = Check(prefix, OtherContinuation); + Require(changedAddress != original, "changing the continuation did not change the reference pixels"); + Continuation[1] = OtherContinuation[1]; + Require(Check(prefix, Continuation) == changedAddress, "rewritten continuation kept the old pixels"); + Continuation[1] = 0x7e08020cu; + Require(Check(prefix, Continuation) == original, "restored continuation did not restore the pixels"); + alignas(256) const std::array clipContinuation{0xbf840005u, 0x7e08020cu, 0xf80000cfu, 0x04030201u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; + const auto clipped = Check(prefix, clipContinuation, 0x00400001u, clipContinuation.size()); + Require(clipped != original && std::count(clipped.begin(), clipped.end(), Kept) > kept, "a clip distance exported by the continuation did not clip the triangle"); + const auto culled = Check(prefix, clipContinuation, 0x00400100u, clipContinuation.size()); + Require(culled == original, "a mixed-sign cull distance discarded the triangle"); + ControlStateDraws(prefix); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + VertexTailDraws(); + AgcDriverShutdown_nid_postfix(); + std::puts("vertex user-data tail 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/ZeroStrideVertexFetch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp index a308a40635..5715c39d27 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroStrideVertexFetch.cpp @@ -78,8 +78,8 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) { const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest); Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute"); if (device.Target().registrationModules) { - if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words(); - Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module"); + if (registrationModule.empty()) registrationModule = vertexResult.spirv.Copy(); + Require(std::ranges::equal(vertexResult.spirv.Words(), registrationModule), std::string(value.what) + ": registration modules: the V# specialized the vertex module"); } const auto vertexPush = static_cast(vertexResult.pushConstants.size()); diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 8dc5f2ad78..15cc83db67 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -374,7 +374,7 @@ std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* paramete std::uint32_t mappingFamily = 0; if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); - if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); + if (mappingFamily > 1 && (mappingFamily != 255 || channels != 1)) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0, 1 or a mono stream's 255)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index f1918902d1..fba6d9efbe 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -898,15 +898,9 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -int APS5_VABI _ZNSt8ios_base7_AddstdEPS__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); } - int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { void* result = nullptr; if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; @@ -957,16 +951,11 @@ void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* 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; } -int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { - return SignalCondition_nid_no_patch(handle); -} - void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } @@ -975,11 +964,6 @@ int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { return _Cnd_init_nid_postfix(handle); } -int APS5_VABI _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZTVSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -995,11 +979,6 @@ int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_ return 0; } -int APS5_VABI _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _ZNSt8time_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE7_GetcatEPPKNSt6locale5facetEPKS5__nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 5f56318a14..c3b44b3d75 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -178,6 +178,12 @@ int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t service_label, const return SCE_APP_CONTENT_ERROR_NOT_FOUND; } +int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label) { + (void)service_label; + if (!entitlement_label) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; +} + int APS5_VABI sceAppContentGetAddcontDownloadProgress(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index e499cbf081..acc471f504 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOutSdl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp @@ -17,6 +18,17 @@ target_link_libraries(libSceAudioOut PRIVATE libc libkernel SDL2-static) configure_windows_unwind(libSceAudioOut) if(BUILD_TESTING) + add_test_executable(audio_out_initialization_tests tests/AudioOutInitialization.cpp src/AudioOutSdl.cpp) + target_include_directories(audio_out_initialization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_initialization_tests PRIVATE libc) + configure_windows_unwind(audio_out_initialization_tests) + add_test(NAME audio_out_initialization COMMAND audio_out_initialization_tests) + set_tests_properties(audio_out_initialization PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(audio_out_initialization PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_pad_mix_tests tests/AudioOut2PadMix.cpp src/AudioOut2PadMix.cpp) target_include_directories(audio_out2_pad_mix_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_pad_mix_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp new file mode 100644 index 0000000000..f0e8c87cc4 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/include/AudioOutSdl.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_INCLUDE_AUDIOOUTSDL_HPP + +int InitializeSdlAudio_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 923d459f2f..fcd7d359f8 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -16,6 +16,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" static constexpr int PORT_TYPE_MAIN = 0; static constexpr int PORT_TYPE_BGM = 1; @@ -108,7 +109,7 @@ static bool ensureSdlAudio() { if (g_sdlInitialized) { return true; } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { return false; } g_sdlInitialized = true; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 383ad018f7..3f88277444 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -14,6 +14,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "AudioOut2Internal.hpp" +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" // An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) // to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so @@ -106,7 +107,7 @@ static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now static void DeviceCallback(void* userdata, Uint8* stream, int length); static void OpenDevice(AudioOut2Context& context) { - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + if (InitializeSdlAudio_nid_no_patch() < 0) { AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError()); return; } @@ -134,7 +135,7 @@ static void CloseDevice(SDL_AudioDeviceID device) { } static void OpenPadDevice(AudioOut2Context& context) { - if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return; + if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && InitializeSdlAudio_nid_no_patch() < 0) return; const int count = SDL_GetNumAudioDevices(0); for (int index = 0; index < count; index++) { const char* listed = SDL_GetAudioDeviceName(index, 0); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index 6ac66b3741..93afb2dabf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -38,4 +38,9 @@ int APS5_VABI sceAudioOut2EnableChat(void) { return 0; } +int APS5_VABI sceAudioOut2DisableChat(void) { + if (g_chatEnabled.exchange(false)) AUDIOOUT2_TRACE("t=%.3f DisableChat: chat audio is no longer allowed alongside the title's\n", AudioOut2TraceSeconds()); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp new file mode 100644 index 0000000000..45f41f5a43 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOutSdl.cpp @@ -0,0 +1,10 @@ +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" + +#include "SDL.h" +#include + +int InitializeSdlAudio_nid_no_patch() { + static std::mutex initializationMutex; + std::lock_guard lock(initializationMutex); + return SDL_InitSubSystem(SDL_INIT_AUDIO); +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 7482925465..b91b5d29df 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAudioOut2MasteringInit(std::uint32_t); int APS5_VABI sceAudioOut2MasteringTerm(); int APS5_VABI sceAudioOut2MasteringSetParam(const void*, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioOut2EnableChat(); +int APS5_VABI sceAudioOut2DisableChat(); } static void Require(bool value, const char* message) { @@ -64,6 +65,9 @@ void TestMasteringSetParam() { void TestEnableChat() { Require(sceAudioOut2EnableChat() == 0, "enabling chat must be accepted"); Require(sceAudioOut2EnableChat() == 0, "enabling chat again must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat must be accepted"); + Require(sceAudioOut2DisableChat() == 0, "disabling chat again must be accepted"); + Require(sceAudioOut2EnableChat() == 0, "enabling chat after disabling it must be accepted"); } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp new file mode 100644 index 0000000000..8ae9cd327c --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutInitialization.cpp @@ -0,0 +1,65 @@ +#define SDL_MAIN_HANDLED +#include "prx/libSceAudioOut/include/AudioOutSdl.hpp" +#include "SDL.h" + +#include +#include +#include +#include +#include +#include + +namespace { +std::mutex stateMutex; +std::condition_variable stateChanged; +int calls = 0; +int active = 0; +int peak = 0; +bool secondEntered = false; +bool secondReady = false; +bool validFlags = true; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32 flags) { + std::unique_lock lock(stateMutex); + validFlags = validFlags && flags == SDL_INIT_AUDIO; + const int call = ++calls; + ++active; + if (active > peak) peak = active; + stateChanged.notify_all(); + if (call == 1) { + stateChanged.wait(lock, [] { return secondReady; }); + stateChanged.wait_for(lock, std::chrono::milliseconds(500), [] { return secondEntered; }); + } else { + secondEntered = true; + stateChanged.notify_all(); + } + --active; + return call == 1 ? -1 : 0; +} + +int main() { + int firstResult = 99; + int secondResult = 99; + std::thread first([&] { firstResult = InitializeSdlAudio_nid_no_patch(); }); + { + std::unique_lock lock(stateMutex); + stateChanged.wait(lock, [] { return calls == 1; }); + } + std::thread second([&] { + { + std::lock_guard lock(stateMutex); + secondReady = true; + stateChanged.notify_all(); + } + secondResult = InitializeSdlAudio_nid_no_patch(); + }); + first.join(); + second.join(); + if (peak != 1 || calls != 2 || active != 0 || !validFlags || firstResult != -1 || secondResult != 0) { + std::fprintf(stderr, "audio initialization overlap or return-value mismatch: peak=%d calls=%d results=%d,%d\n", peak, calls, firstResult, secondResult); + return 1; + } + std::puts("Serialized audio initialization: PASS"); + return 0; +} diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 768358e690..7778568ddc 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -151,6 +151,7 @@ public: void Reset(double milliseconds) { base = milliseconds; running = false; + paused = false; } void Pause() { diff --git a/core/libs/prx/libSceCesCs-module/CMakeLists.txt b/core/libs/prx/libSceCesCs-module/CMakeLists.txt new file mode 100644 index 0000000000..64ae3a7a2e --- /dev/null +++ b/core/libs/prx/libSceCesCs-module/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceCesCs/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs-module) diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index 82fbba577e..674db3453e 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -1,14 +1,17 @@ -add_library(libSceCesCs SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceCesCs PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceCesCs) +include(${CMAKE_CURRENT_SOURCE_DIR}/CesLibrary.cmake) +add_sce_ces_library(libSceCesCs) if(BUILD_TESTING) add_test_executable(guest_ces_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) target_include_directories(guest_ces_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ces_tests PRIVATE libSceCesCs) add_test(NAME guest_ces COMMAND guest_ces_tests) + add_test_executable(guest_ces_module_tests ${LIBS_INCLUDE_DIR}/tests/GuestCes.cpp) + target_include_directories(guest_ces_module_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ces_module_tests PRIVATE libSceCesCs-module) + add_test(NAME guest_ces_module COMMAND guest_ces_module_tests) if(WIN32) - set_tests_properties(guest_ces PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ces guest_ces_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 new file mode 100644 index 0000000000..dcad1c7bd6 --- /dev/null +++ b/core/libs/prx/libSceCesCs/CesLibrary.cmake @@ -0,0 +1,5 @@ +function(add_sce_ces_library target) + add_library(${target} SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Export.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 489bc94b31..215e941d22 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -115,6 +115,11 @@ const uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void) { return CP1251_PROFILE; } +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { + if (!sheet) return nullptr; + return sheet; +} + int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { (void)profile; if (!context) return CES_ERROR_INVALID_PARAMETER; diff --git a/core/libs/prx/libSceConvertKeycode/CMakeLists.txt b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt new file mode 100644 index 0000000000..d4f82eb5f7 --- /dev/null +++ b/core/libs/prx/libSceConvertKeycode/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceConvertKeycode SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceConvertKeycode PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceConvertKeycode) + +if(BUILD_TESTING) + add_test_executable(guest_convert_keycode_tests ${LIBS_INCLUDE_DIR}/tests/GuestConvertKeycode.cpp) + target_include_directories(guest_convert_keycode_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_convert_keycode_tests PRIVATE libSceConvertKeycode libc) + configure_windows_unwind(guest_convert_keycode_tests) + add_test(NAME guest_convert_keycode COMMAND guest_convert_keycode_tests) + if(WIN32) + set_tests_properties(guest_convert_keycode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index ec43b33b03..29c849a7e1 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "SceTypes.hpp" #include "HitLog.hpp" #include "prx/libc/include/General.hpp" @@ -6,6 +8,18 @@ namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); + +char16_t UsCharacter(std::uint16_t keycode) { + static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; + static constexpr char16_t kKeypad[] = u"/*-+\0" u"1234567890."; + if (keycode >= 0x04 && keycode <= 0x1D) return static_cast(u'a' + (keycode - 0x04)); + if (keycode >= 0x1E && keycode <= 0x26) return static_cast(u'1' + (keycode - 0x1E)); + if (keycode == 0x27) return u'0'; + if (keycode == 0x2C) return u' '; + if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; + if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; + return 0; +} } // namespace extern "C" { @@ -27,8 +41,11 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { return 0; } -int APS5_VABI sceConvertKeycodeGetCharacter(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { + if (character == nullptr) return kErrInvalidAddress; + if (state != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); + if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); + *character = UsCharacter(keycode); return 0; } diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 51df883d95..a727830404 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include #include #include @@ -207,11 +208,8 @@ static void UnpinStack(const void* context, std::uint64_t bytes) { extern "C" void Aps5FiberSwitchStack_nid_no_patch(void** save, void* load); extern "C" void Aps5FiberTrampoline_nid_no_patch(); -asm(R"( - .text - .globl Aps5FiberSwitchStack_nid_no_patch - .type Aps5FiberSwitchStack_nid_no_patch, @function -Aps5FiberSwitchStack_nid_no_patch: +asm(".text\n" + APS5_ASM_FUNCTION("Aps5FiberSwitchStack_nid_no_patch") R"( push %rbp push %rbx push %r12 @@ -233,17 +231,13 @@ Aps5FiberSwitchStack_nid_no_patch: pop %rbx pop %rbp ret - .size Aps5FiberSwitchStack_nid_no_patch, .-Aps5FiberSwitchStack_nid_no_patch - - .globl Aps5FiberTrampoline_nid_no_patch - .type Aps5FiberTrampoline_nid_no_patch, @function -Aps5FiberTrampoline_nid_no_patch: +)" APS5_ASM_FUNCTION_END("Aps5FiberSwitchStack_nid_no_patch") + APS5_ASM_FUNCTION("Aps5FiberTrampoline_nid_no_patch") R"( mov %r12, %rdi and $-16, %rsp - call Aps5FiberMain_nid_no_patch + call )" APS5_ASM_SYMBOL("Aps5FiberMain_nid_no_patch") R"( ud2 - .size Aps5FiberTrampoline_nid_no_patch, .-Aps5FiberTrampoline_nid_no_patch -)"); +)" APS5_ASM_FUNCTION_END("Aps5FiberTrampoline_nid_no_patch")); struct InitialFrame { std::uint32_t mxcsr; @@ -511,7 +505,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { if (fiber_info->size != sizeof(FiberInfo)) return SCE_FIBER_ERROR_INVALID; fiber_info->entry = reinterpret_cast(fiber->entry); fiber_info->arg_on_initialize = fiber->argOnInitialize; - fiber_info->addr_context = fiber->context; + fiber_info->addr_context = SizedWindow(fiber); fiber_info->size_context = fiber->contextSize; std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 786bf30997..36dfae81ec 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -75,7 +75,6 @@ static void EraseCompletions(const std::vector& ids) { } static int CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { - if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function); if (option != nullptr) NotImplemented_nid_no_patch(function); bool queued = false; { @@ -90,6 +89,7 @@ static int CompleteAsync(const char* function, int req_id, const Http2AsyncOptio } } if (!queued) return ERROR_INVALID_ID; + if (kqueue_option == nullptr) return 0; if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { { std::lock_guard lock(g_completionsMutex); diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 489c43cc1a..5f45b0907c 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -22,4 +22,15 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_keyboard 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}) + target_link_libraries(guest_ime_open_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_open_tests) + add_test(NAME guest_ime_open COMMAND guest_ime_open_tests) + if(WIN32) + set_tests_properties(guest_ime_open PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 4675c980f5..d686445211 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,52 +1,346 @@ +#include #include #include #include +#include #include +#include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" namespace { +constexpr int ErrorBusy = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); +constexpr int ErrorInvalidText = static_cast(0x80bc0006u); constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); +constexpr int ErrorInvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int ErrorInvalidInputMethod = static_cast(0x80bc0014u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); -constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorInvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int ErrorInvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int ErrorInvalidPosX = static_cast(0x80bc0018u); +constexpr int ErrorInvalidPosY = static_cast(0x80bc0019u); +constexpr int ErrorInvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int ErrorInvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int ErrorInvalidExtended = static_cast(0x80bc001cu); +constexpr int ErrorInvalidWork = static_cast(0x80bc0020u); +constexpr int ErrorInvalidHandler = static_cast(0x80bc0022u); constexpr int ErrorNoResourceId = static_cast(0x80bc0023u); 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 uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; +constexpr uint32_t OptionMultiline = 0x00000001; constexpr uint32_t OptionUseOver2K = 0x00004000; +constexpr uint32_t EnterLabelLast = 3; +constexpr uint32_t AlignmentLast = 2; +constexpr uint32_t PanelPriorityLast = 3; +constexpr uint32_t MaxTextLength = 2048; constexpr int32_t UserIdInvalid = -1; +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; +constexpr uint32_t CaretMoveLeft = 1; +constexpr uint32_t CaretMoveRight = 2; + +struct Session { + Param param{}; + std::u16string text; + std::uint32_t caret = 0; + std::deque events; +}; + std::mutex g_keyboardMutex; std::set g_openKeyboards; +std::mutex g_imeMutex; +std::optional g_session; + +void PanelSize(const Param& param, uint32_t* width, uint32_t* height) { + const uint32_t scale = (param.option & OptionUseOver2K) != 0 ? 2 : 1; + *width = (param.type == TypeNumber ? 370u : 793u) * scale; + *height = (param.type == TypeNumber ? 402u : 408u) * scale; +} + +bool IsHighSurrogate(char16_t unit) { return unit >= 0xd800 && unit <= 0xdbff; } +bool IsLowSurrogate(char16_t unit) { return unit >= 0xdc00 && unit <= 0xdfff; } + +bool ValidText(const char16_t* text, std::size_t length, bool multiline) { + for (std::size_t i = 0; i < length; ++i) { + const char16_t unit = text[i]; + if (!multiline && (unit == u'\n' || unit == u'\r')) return false; + if (IsLowSurrogate(unit)) return false; + if (IsHighSurrogate(unit)) { + if (i + 1 >= length || !IsLowSurrogate(text[i + 1])) return false; + ++i; + } + } + return true; +} + +std::size_t TextLength(const char16_t* text, std::size_t limit) { + std::size_t length = 0; + while (length < limit && text[length] != u'\0') ++length; + return length; +} + +std::u16string DecodeUtf8(const char* text, std::size_t size) { + std::u16string units; + for (std::size_t i = 0; i < size;) { + const auto lead = static_cast(text[i]); + std::uint32_t codePoint = 0; + std::size_t extra = 0; + if (lead < 0x80) codePoint = lead; + else if ((lead & 0xe0) == 0xc0) { codePoint = lead & 0x1f; extra = 1; } + else if ((lead & 0xf0) == 0xe0) { codePoint = lead & 0x0f; extra = 2; } + else if ((lead & 0xf8) == 0xf0) { codePoint = lead & 0x07; extra = 3; } + else throw std::invalid_argument("IME host input: invalid UTF-8 lead byte"); + if (extra != 0 && i + extra >= size) throw std::invalid_argument("IME host input: truncated UTF-8 sequence"); + for (std::size_t k = 1; k <= extra; ++k) { + const auto next = static_cast(text[i + k]); + if ((next & 0xc0) != 0x80) throw std::invalid_argument("IME host input: invalid UTF-8 continuation byte"); + codePoint = (codePoint << 6) | (next & 0x3f); + } + i += extra + 1; + if (codePoint >= 0x10000) { + codePoint -= 0x10000; + units.push_back(static_cast(0xd800 + (codePoint >> 10))); + units.push_back(static_cast(0xdc00 + (codePoint & 0x3ff))); + } else { + units.push_back(static_cast(codePoint)); + } + } + return units; +} + +void SyncBuffers(Session& session) { + const std::size_t bytes = session.text.size() * sizeof(char16_t); + std::memcpy(session.param.input_text_buffer, session.text.data(), bytes); + session.param.input_text_buffer[session.text.size()] = u'\0'; + auto* work = static_cast(session.param.work); + std::memcpy(work, session.text.data(), bytes); + work[session.text.size()] = u'\0'; +} + +void QueueText(Session& session, uint32_t id, uint32_t index, int32_t delta) { + ImeEvent event{}; + event.id = id; + event.param.text.str = static_cast(session.param.work); + event.param.text.caret_index = session.caret; + event.param.text.area_num = 1; + event.param.text.text_area[0].mode = TextAreaMode::Edit; + event.param.text.text_area[0].index = index; + event.param.text.text_area[0].length = delta; + session.events.push_back(event); +} + +void QueueCaretSteps(Session& session, uint32_t direction, std::size_t steps) { + for (std::size_t i = 0; i < steps; ++i) { + ImeEvent event{}; + event.id = EventUpdateCaret; + event.param.caret_move = direction; + session.events.push_back(event); + } +} + +void Insert(Session& session, std::u16string units) { + const bool multiline = (session.param.option & OptionMultiline) != 0; + std::u16string accepted; + for (const char16_t unit : units) { + if (!multiline && (unit == u'\n' || unit == u'\r')) continue; + accepted.push_back(unit); + } + 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; + const uint32_t index = session.caret; + session.text.insert(index, accepted, 0, count); + session.caret += static_cast(count); + SyncBuffers(session); + QueueText(session, EventUpdateText, index, static_cast(count)); +} + +std::size_t UnitsBefore(const Session& session) { + const uint32_t caret = session.caret; + if (caret == 0) return 0; + return caret >= 2 && IsLowSurrogate(session.text[caret - 1]) && IsHighSurrogate(session.text[caret - 2]) ? 2 : 1; +} + +std::size_t UnitsAfter(const Session& session) { + const uint32_t caret = session.caret; + if (caret >= session.text.size()) return 0; + return caret + 1 < session.text.size() && IsHighSurrogate(session.text[caret]) && IsLowSurrogate(session.text[caret + 1]) ? 2 : 1; +} + +void 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; + } + case ImeHostInputKind::Backspace: { + const std::size_t units = UnitsBefore(session); + if (units == 0) return; + session.caret -= static_cast(units); + session.text.erase(session.caret, units); + SyncBuffers(session); + QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + return; + } + case ImeHostInputKind::Delete: { + const std::size_t units = UnitsAfter(session); + if (units == 0) return; + session.text.erase(session.caret, units); + SyncBuffers(session); + QueueText(session, EventUpdateText, session.caret, -static_cast(units)); + return; + } + case ImeHostInputKind::Left: { + const std::size_t units = UnitsBefore(session); + session.caret -= static_cast(units); + QueueCaretSteps(session, CaretMoveLeft, units); + return; + } + case ImeHostInputKind::Right: { + const std::size_t units = UnitsAfter(session); + session.caret += static_cast(units); + QueueCaretSteps(session, CaretMoveRight, units); + return; + } + case ImeHostInputKind::Home: { + const std::size_t units = session.caret; + session.caret = 0; + QueueCaretSteps(session, CaretMoveLeft, units); + return; + } + 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; + } + case ImeHostInputKind::Enter: + if ((session.param.option & OptionMultiline) != 0) { + Insert(session, u"\n"); + } else { + QueueText(session, EventPressEnter, session.caret, 0); + } + return; + case ImeHostInputKind::Escape: + QueueText(session, EventPressClose, session.caret, 0); + return; + } + throw std::invalid_argument("IME host input: unknown input kind"); +} + +int ValidateOpen(const Param* param, const ExtendedParam* extended) { + if (!param) return ErrorInvalidAddress; + if (param->user_id == UserIdInvalid) return ErrorInvalidUserId; + if (param->type > TypeNumber) return ErrorInvalidType; + if (param->enter_label > EnterLabelLast) return ErrorInvalidEnterLabel; + if (param->input_method != 0) return ErrorInvalidInputMethod; + if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; + if (param->max_text_length == 0 || param->max_text_length > MaxTextLength) return ErrorInvalidMaxTextLength; + if (!param->input_text_buffer) return ErrorInvalidInputTextBuffer; + const float maxX = (param->option & OptionUseOver2K) != 0 ? 3840.0f : 1920.0f; + const float maxY = (param->option & OptionUseOver2K) != 0 ? 2160.0f : 1080.0f; + if (!(param->posx >= 0.0f && param->posx < maxX)) return ErrorInvalidPosX; + if (!(param->posy >= 0.0f && param->posy < maxY)) return ErrorInvalidPosY; + if (param->horizontal_alignment > AlignmentLast) return ErrorInvalidHorizontalAlignment; + if (param->vertical_alignment > AlignmentLast) return ErrorInvalidVerticalAlignment; + if (extended) { + if (extended->priority > PanelPriorityLast) return ErrorInvalidExtended; + for (const std::int8_t byte : extended->reserved) { + if (byte != 0) return ErrorInvalidExtended; + } + } + if (!param->work || (reinterpret_cast(param->work) & 3u) != 0) return ErrorInvalidWork; + if (!param->handler) return ErrorInvalidHandler; + for (const std::int8_t byte : param->reserved) { + if (byte != 0) return ErrorInvalidReserved; + } + const std::size_t length = TextLength(param->input_text_buffer, param->max_text_length); + if (!ValidText(param->input_text_buffer, length, (param->option & OptionMultiline) != 0)) return ErrorInvalidText; + return 0; +} } extern "C" { +void ImePublishInput_nid_postfix(const ImeHostInput& input) { + std::lock_guard lock(g_imeMutex); + if (g_session) Apply(*g_session, input); +} + +bool ImeCapturesKeyboard_nid_postfix() { + std::lock_guard lock(g_imeMutex); + return g_session.has_value(); +} + +int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { + 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.param = *param; + 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); + ImeEvent open{}; + open.id = EventOpen; + open.param.rect.x = param->posx; + open.param.rect.y = param->posy; + PanelSize(*param, &open.param.rect.width, &open.param.rect.height); + session.events.push_back(open); + g_session = std::move(session); + return 0; +} + int APS5_VABI sceImeClose_nid_postfix(void) { - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + g_session.reset(); + return 0; } int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form) { - (void)form; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!form) return ErrorInvalidAddress; + const Param& param = g_session->param; + *form = {}; + form->type = param.type; + form->posx = param.posx; + form->posy = param.posy; + form->horizontal_alignment = param.horizontal_alignment; + form->vertical_alignment = param.vertical_alignment; + PanelSize(param, &form->width, &form->height); + return 0; } int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) { if (!param || !width || !height) return ErrorInvalidAddress; if (param->type > TypeNumber) return ErrorInvalidType; if ((param->option & ~ValidOptions) != 0) return ErrorInvalidOption; - const uint32_t scale = (param->option & OptionUseOver2K) != 0 ? 2 : 1; - *width = (param->type == TypeNumber ? 370u : 793u) * scale; - *height = (param->type == TypeNumber ? 402u : 408u) * scale; + PanelSize(*param, width, height); return 0; } @@ -86,13 +380,6 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } -int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { - (void)param; - (void)extended; - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI sceImeParamInit(Param* param) { if (!param) return; std::memset(param, 0, sizeof(*param)); @@ -100,25 +387,49 @@ void APS5_VABI sceImeParamInit(Param* param) { } int APS5_VABI sceImeSetCaret(const Caret* caret) { - (void)caret; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!caret) return ErrorInvalidAddress; + if (caret->index > g_session->text.size()) return ErrorInvalidParam; + g_session->caret = caret->index; + return 0; } int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length) { - (void)text; - (void)length; - return ErrorNotOpened; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!text) return ErrorInvalidAddress; + Session& session = *g_session; + 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); + if (session.caret > session.text.size()) session.caret = static_cast(session.text.size()); + SyncBuffers(session); + return 0; } int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry) { - (void)mode; - (void)geometry; - return ErrorNotOpened; + (void)mode; + std::lock_guard lock(g_imeMutex); + if (!g_session) return ErrorNotOpened; + if (!geometry) return ErrorInvalidAddress; + return 0; } int APS5_VABI sceImeUpdate(EventHandler handler) { - if (!handler) APS5_INVALID_ARG_EX; - return 0; + if (!handler) APS5_INVALID_ARG_EX; + for (;;) { + ImeEvent event{}; + void* arg = nullptr; + { + 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; + } + handler(arg, &event); + } } } diff --git a/core/libs/prx/libSceIme/include/ImeInput.hpp b/core/libs/prx/libSceIme/include/ImeInput.hpp new file mode 100644 index 0000000000..4212dd61ea --- /dev/null +++ b/core/libs/prx/libSceIme/include/ImeInput.hpp @@ -0,0 +1,26 @@ +#ifndef CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEIME_INCLUDE_IMEINPUT_HPP + +#include + +enum class ImeHostInputKind : std::uint32_t { + Text, + Backspace, + Delete, + Left, + Right, + Home, + End, + Enter, + Escape, +}; + +struct ImeHostInput { + ImeHostInputKind kind = ImeHostInputKind::Text; + char text[32] = {}; +}; + +extern "C" void ImePublishInput_nid_postfix(const ImeHostInput& input); +extern "C" bool ImeCapturesKeyboard_nid_postfix(); + +#endif diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 2bb6273e75..0a16e2d75e 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -29,7 +29,7 @@ KeyboardData snapshot() { KeyboardData data{}; data.timestamp = sceKernelGetProcessTime(); data.connected = connected; - data.length = keyCount; + data.length = std::max(keyCount, 1); data.led = led; data.modifier_key = modifiers; std::copy_n(keys.begin(), keyCount, data.key_code); diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 802e1da750..cb05958e16 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -54,7 +54,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 17) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardReadState(KEYBOARD_HANDLE, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); KeyboardInput input; @@ -66,7 +66,7 @@ int main() { input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_RETURN, KMOD_NUM | KMOD_LSHIFT), 7); input.HandleEvent(KeyEvent(SDL_KEYUP, SDL_SCANCODE_A, KMOD_NUM | KMOD_LSHIFT), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 16) == 4); - Require(data[0].length == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); + Require(data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 2 && data[0].led == KEYBOARD_LED_NUM_LOCK); Require(data[1].length == 1 && data[1].key_code[0] == 0x04); Require(data[2].length == 2 && data[2].key_code[0] == 0x04 && data[2].key_code[1] == 0x28); Require(data[3].length == 1 && data[3].key_code[0] == 0x28 && data[3].modifier_key == 2); @@ -84,7 +84,7 @@ int main() { window.window.event = SDL_WINDOWEVENT_FOCUS_LOST; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1); - Require(data[0].connected && data[0].length == 0 && data[0].modifier_key == 0); + Require(data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0 && data[0].modifier_key == 0); input.HandleEvent(KeyEvent(SDL_KEYDOWN, SDL_SCANCODE_B), 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; @@ -92,7 +92,7 @@ int main() { Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 0); window.window.event = SDL_WINDOWEVENT_CLOSE; input.HandleEvent(window, 7); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && !data[0].connected && data[0].length == 1 && data[0].key_code[0] == 0); window.window.event = SDL_WINDOWEVENT_FOCUS_GAINED; input.HandleEvent(window, 7); Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].connected); @@ -142,6 +142,6 @@ int main() { Require(sceKeyboardReadState(KEYBOARD_HANDLE, data) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_ERROR_INVALID_HANDLE); Require(sceKeyboardOpen(1, 0, 0, nullptr) == KEYBOARD_HANDLE); - Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 0); + Require(sceKeyboardRead(KEYBOARD_HANDLE, data, 1) == 1 && data[0].length == 1 && data[0].key_code[0] == 0); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); } diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 17cbde0adf..b5dde2fe3f 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp Spawn.cpp UserContext.cpp) +add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp SignalList.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) @@ -115,4 +115,14 @@ if(BUILD_TESTING) set_tests_properties(guest_fminf PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_signal_list_tests ${LIBS_INCLUDE_DIR}/tests/GuestSignalList.cpp) + target_include_directories(guest_signal_list_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_signal_list_tests PRIVATE libSceLibcInternal) + configure_windows_unwind(guest_signal_list_tests) + add_test(NAME guest_signal_list COMMAND guest_signal_list_tests) + set_tests_properties(guest_signal_list PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_signal_list PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibcInternal/SignalList.cpp b/core/libs/prx/libSceLibcInternal/SignalList.cpp new file mode 100644 index 0000000000..c51fa70fe8 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/SignalList.cpp @@ -0,0 +1,40 @@ +extern "C" { + +extern const char* const sys_siglist_nid_postfix[32] = { + "Signal 0", + "Hangup", + "Interrupt", + "Quit", + "Illegal instruction", + "Trace/BPT trap", + "Abort trap", + "EMT trap", + "Floating point exception", + "Killed", + "Bus error", + "Segmentation fault", + "Bad system call", + "Broken pipe", + "Alarm clock", + "Terminated", + "Urgent I/O condition", + "Suspended (signal)", + "Suspended", + "Continued", + "Child exited", + "Stopped (tty input)", + "Stopped (tty output)", + "I/O possible", + "Cputime limit exceeded", + "Filesize limit exceeded", + "Virtual timer expired", + "Profiling timer expired", + "Window size changes", + "Information request", + "User defined signal 1", + "User defined signal 2", +}; + +extern const int sys_nsig_nid_postfix = sizeof(sys_siglist_nid_postfix) / sizeof(sys_siglist_nid_postfix[0]); + +} diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 7c34334844..e03f88e831 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -14,6 +14,7 @@ #include #endif #include +#include #include #include #include @@ -82,6 +83,7 @@ constexpr int NET_EPOLL_CTL_DEL = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; +constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; @@ -91,6 +93,7 @@ constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; constexpr int NET_SO_SCE_RCVTIMEO = 0x1106; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; +constexpr int NET_MSG_DONTWAIT = 0x80; constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 @@ -106,8 +109,8 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int native_error() { - switch (WSAGetLastError()) { +int net_error(int code) { + switch (code) { case WSAEWOULDBLOCK: return NET_EAGAIN; case WSAEINPROGRESS: return NET_EINPROGRESS; case WSAEALREADY: return NET_EALREADY; @@ -132,6 +135,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; using NativeLength = socklen_t; @@ -139,8 +143,8 @@ 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 native_error() { - switch (errno) { +int net_error(int code) { + switch (code) { case EAGAIN: return NET_EAGAIN; #if EWOULDBLOCK != EAGAIN case EWOULDBLOCK: return NET_EAGAIN; @@ -168,6 +172,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(errno); } #endif struct NativeSocketHandle { @@ -243,6 +248,7 @@ struct Sock { bool aborted = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; + int reported_error = 0; }; #ifdef _WIN32 @@ -468,6 +474,21 @@ int receive_error(const Sock& socket) { return native_error(); } +bool message_flags_supported(int flags, int accepted) { +#ifdef _WIN32 + accepted &= ~NET_MSG_DONTWAIT; +#endif + return (flags & ~accepted) == 0; +} + +int native_message_flags(int flags) { + int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; +#ifndef _WIN32 + if (flags & NET_MSG_DONTWAIT) native |= MSG_DONTWAIT; +#endif + return native; +} + } // namespace extern "C" { @@ -758,14 +779,51 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { return descriptor; } +static std::uint16_t& native_port(sockaddr_storage& address) { + return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; +} + +static int disconnect_datagram(const Sock& socket) { + const auto value = socket.native->value; + sockaddr_storage peer{}; + NativeLength peer_length = sizeof(peer); + if (::getpeername(value, reinterpret_cast(&peer), &peer_length) != 0) return 0; + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + if (::getsockname(value, reinterpret_cast(&local), &local_length) != 0) return fail(native_error()); +#ifdef _WIN32 + sockaddr_storage unspecified{}; + unspecified.ss_family = local.ss_family; + if (::connect(value, reinterpret_cast(&unspecified), local_length) != 0) return fail(native_error()); +#else + sockaddr unspecified{}; + unspecified.sa_family = AF_UNSPEC; + if (::connect(value, &unspecified, sizeof(unspecified)) != 0) return fail(native_error()); +#endif + sockaddr_storage current{}; + NativeLength current_length = sizeof(current); + if (::getsockname(value, reinterpret_cast(¤t), ¤t_length) != 0) return fail(native_error()); + if (native_port(current) != 0) return 0; + sockaddr_storage wildcard{}; + wildcard.ss_family = local.ss_family; + native_port(wildcard) = native_port(local); + if (::bind(value, reinterpret_cast(&wildcard), local_length) != 0) return fail(native_error()); + return 0; +} + int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { Sock socket; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it == g_socks.end()) return fail(NET_EBADF); + it->second.reported_error = 0; socket = it->second; } + if (socket.type == NET_SOCK_DGRAM && addr && addrlen >= 2 && static_cast(addr)[1] == 0) { + if (const int error = disconnect_datagram(socket)) return error; + return fail(NET_EAFNOSUPPORT); + } sockaddr_storage native{}; NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); @@ -778,7 +836,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { } int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -790,7 +848,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); - const int native_flags = flags == 2 ? MSG_PEEK : 0; + const int native_flags = native_message_flags(flags); const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); @@ -799,7 +857,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* from, uint32_t* fromlen) { - if (flags != 0 && flags != 2) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); if (from && !fromlen) return fail(NET_EINVAL); @@ -814,7 +872,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const int native_flags = flags == 2 ? MSG_PEEK : 0; + 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); #ifdef _WIN32 @@ -826,7 +884,7 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* } int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -836,12 +894,12 @@ 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); + const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); return result >= 0 ? result : fail(native_error()); } int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, const void* to, uint32_t tolen) { - if (flags != 0) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); if (!buf && len) return fail(NET_EINVAL); if (len > INT_MAX) return fail(NET_EMSGSIZE); Sock socket; @@ -854,13 +912,13 @@ 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); + result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); } 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, + result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), reinterpret_cast(&destination), destination_length); } return result >= 0 ? result : fail(native_error()); @@ -887,7 +945,7 @@ int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { const auto total = message_length(msg); if (total < 0) return total; - if (flags != 0 && flags != NET_MSG_PEEK) return fail(NET_EOPNOTSUPP); + if (!message_flags_supported(flags, NET_MSG_PEEK | NET_MSG_DONTWAIT)) return fail(NET_EOPNOTSUPP); Sock socket; { std::lock_guard lk(g_mutex); @@ -905,7 +963,7 @@ 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()), - flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); if (received < 0) { #ifdef _WIN32 if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); @@ -1001,6 +1059,21 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint 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_ERROR: { + int pending = 0; + NativeLength size = sizeof(pending); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size) != 0) + return fail(native_error()); +#ifdef _WIN32 + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + if (pending != 0 && pending == it->second.reported_error) pending = 0; + else if (pending != 0) it->second.reported_error = pending; +#endif + value = pending == 0 ? 0 : net_error(pending); + break; + } default: return fail(NET_EOPNOTSUPP); } std::memcpy(optval, &value, sizeof(value)); @@ -1065,6 +1138,25 @@ int APS5_VABI sceNetEpollCreate(const char* name, int flags) { return id; } +int APS5_VABI sceNetShowNetstatWithMemory(int memid) { + struct SocketStatus { int id; int family; int type; bool nonblock; bool bound; bool listening; }; + std::vector sockets; + std::size_t poolSize = 0; + { + std::lock_guard lk(g_mutex); + const auto pool = g_pools.find(memid); + if (pool == g_pools.end()) return fail(NET_EBADF); + 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}); + } + std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); + for (const auto& socket : sockets) + std::fprintf(stdout, "socket=%d family=%d type=%d nonblock=%d bound=%d listening=%d\n", socket.id, socket.family, socket.type, int(socket.nonblock), int(socket.bound), int(socket.listening)); + return 0; +} + int APS5_VABI sceNetGetMemoryPoolStats(int memid, NetMemoryPoolStats* stats) { std::lock_guard lk(g_mutex); const auto pool = g_pools.find(memid); @@ -1299,12 +1391,12 @@ int APS5_VABI sceNetResolverDestroy(int rid) { return g_resolvers.erase(rid) != 0 ? 0 : fail(NET_EBADF); } -int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { +int lookup_host(int rid, const char* hostname, const char* func, int family, std::vector>& addresses) { if (!initialize_sockets()) return fail(5); std::uint64_t generation = 0; if (const int error = begin_lookup(rid, &Resolver::abort_ntoa, generation)) return error; addrinfo hints{}; - hints.ai_family = AF_INET; + hints.ai_family = family; addrinfo* results = nullptr; const int result = ::getaddrinfo(hostname, nullptr, &hints, &results); if (result != 0) { @@ -1313,13 +1405,31 @@ int lookup_ipv4(int rid, const char* hostname, const char* func, std::vectorai_next) { - const auto address = reinterpret_cast(entry->ai_addr)->sin_addr.s_addr; + if (entry->ai_family != family) continue; + std::array address{}; + if (family == AF_INET6) std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin6_addr, 16); + else std::memcpy(address.data(), &reinterpret_cast(entry->ai_addr)->sin_addr, 4); if (std::find(addresses.begin(), addresses.end(), address) == addresses.end()) addresses.push_back(address); } ::freeaddrinfo(results); + if (addresses.empty()) { + set_errno(NET_ENOENT); + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); + } return end_lookup(rid, generation, 0); } +int lookup_ipv4(int rid, const char* hostname, const char* func, std::vector& addresses) { + std::vector> found; + if (const int error = lookup_host(rid, hostname, func, AF_INET, found)) return error; + for (const auto& address : found) { + std::uint32_t value = 0; + std::memcpy(&value, address.data(), sizeof(value)); + addresses.push_back(value); + } + return 0; +} + int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { (void)timeout; (void)retry; @@ -1331,6 +1441,17 @@ int APS5_VABI sceNetResolverStartNtoa(int rid, const char* hostname, void* addr, return 0; } +int APS5_VABI sceNetResolverStartNtoa6(int rid, const char* hostname, void* addr, int timeout, int retry, int flags) { + (void)timeout; + (void)retry; + if (flags != 0) throw std::runtime_error("sceNetResolverStartNtoa6: flags " + std::to_string(flags) + " are not supported"); + if (!hostname || !addr) return fail(NET_EINVAL); + std::vector> addresses; + if (const int error = lookup_host(rid, hostname, __func__, AF_INET6, addresses)) return error; + std::memcpy(addr, addresses.front().data(), addresses.front().size()); + return 0; +} + struct NetResolverRecord { std::uint32_t address; std::uint8_t address6[12]; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 5f0ab11e26..c6645e18b1 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -34,6 +34,27 @@ static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; +static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; + +static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) { + if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; + std::lock_guard lock(g_callbackLock); + for (int index = 0; index < MAX_CALLBACKS; ++index) { + if (!callbacks[index]) { + callbacks[index] = func; + *cid = index; + return 0; + } + } + return SCE_NET_CTL_ERROR_CALLBACK_MAX; +} + +static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) { + if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; + std::lock_guard lock(g_callbackLock); + callbacks[cid] = nullptr; + return 0; +} static bool HostHasAddress(int family) { #ifdef _WIN32 @@ -169,26 +190,22 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; - std::lock_guard lock(g_callbackLock); - for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!g_callbacks[index]) { - g_callbacks[index] = func; - *cid = index; - return 0; - } - } - return SCE_NET_CTL_ERROR_CALLBACK_MAX; + return RegisterCallback(g_callbacks, func, cid); +} + +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { + return RegisterCallback(g_callbacksV6, func, cid); } void APS5_VABI sceNetCtlTerm(void) { } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { - if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; - std::lock_guard lock(g_callbackLock); - g_callbacks[cid] = nullptr; - return 0; + return UnregisterCallback(g_callbacks, cid); +} + +int APS5_VABI sceNetCtlUnregisterCallbackV6(int cid) { + return UnregisterCallback(g_callbacksV6, cid); } } diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index fd9cfd40ff..2e7886f412 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -19,6 +19,7 @@ static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_REVERB = 0x2001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_MASTERING = 0x3000; +static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SAMPLER = 0x4001; static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; @@ -57,11 +58,17 @@ static constexpr std::uint32_t SCE_NGS2_REVERB_VOICE_PARAM_I3DL2 = 0x20010001; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP = 0x40010000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS = 0x40010001; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x40010004; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH = 0x40010005; static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER = 0x40001300; static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_FILTER = 0x13; static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; @@ -275,6 +282,17 @@ struct Ngs2CustomRackOption { }; static_assert(sizeof(Ngs2CustomRackOption) == 1280); +struct Ngs2CustomSamplerRackOption { + Ngs2CustomRackOption custom_rack_option; + std::uint32_t max_channel_works; + std::uint32_t max_waveform_blocks; + std::uint32_t max_atrac9_decoders; + std::uint32_t max_atrac9_channel_works; + std::uint32_t max_ajm_atrac9_decoders; + std::uint32_t max_codec_caches; +}; +static_assert(sizeof(Ngs2CustomSamplerRackOption) == 1304); + struct Ngs2CustomSubmixerRackOption { Ngs2CustomRackOption custom_rack_option; std::uint32_t max_channels; @@ -491,6 +509,33 @@ struct Ngs2CustomVoiceUserFx2Param { }; static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); +struct Ngs2CustomVoiceFilterParam { + Ngs2VoiceParamHeader header; + std::uint32_t type; + std::uint32_t channel_mask; + union { + struct { + float i0; + float i1; + float i2; + float o1; + float o2; + } direct; + struct { + float frequency; + float q; + float level; + std::uint32_t reserved; + std::uint32_t reserved2; + } fcq; + } param; + std::uint32_t reserved3; +}; +static_assert(sizeof(Ngs2CustomVoiceFilterParam) == 40); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, type) == 8); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, channel_mask) == 12); +static_assert(offsetof(Ngs2CustomVoiceFilterParam, param) == 16); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index c77dc52054..af850cd2b8 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -17,10 +17,16 @@ void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; for (std::uint32_t m = 0; m < option.num_modules; m++) { const auto& info = option.module[m]; - if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); - if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; - if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; - if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) { + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + } else if (info.module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) { + if (info.option != nullptr || info.state_size != 0 || info.state_offset != 0) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.extra_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + } else { + throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); + } } for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; @@ -34,10 +40,13 @@ static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, } void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.customModuleIds.resize(option.num_modules); rack.userFx.resize(option.num_modules); rack.userFxCommon.resize(option.num_modules); for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); for (std::uint32_t m = 0; m < option.num_modules; m++) { + rack.customModuleIds[m] = option.module[m].module_id; + if (option.module[m].module_id == SCE_NGS2_CUSTOM_MODULE_ID_FILTER) continue; const auto& module = *reinterpret_cast(option.module[m].option); rack.userFx[m] = module; rack.userFxCommon[m].resize(module.common_size); @@ -58,6 +67,7 @@ void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { void Ngs2CleanupUserFx(Ngs2Rack& rack) { for (std::uint32_t i = 0; i < rack.voices.size(); i++) { for (std::size_t m = 0; m < rack.userFx.size(); m++) { + if (rack.customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; if (rack.userFx[m].cleanup_handler == nullptr) continue; auto context = UserFxContext(rack, m, i); const int result = rack.userFx[m].cleanup_handler(&context); @@ -84,23 +94,53 @@ static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2Cust } void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { - if (voice.rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + const auto rackId = voice.rack->rackId; if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); SetupCustomSubmixer(voice, ParamAs(param)); return; } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } + const auto index = param.id & 0x1f; + if (index >= voice.rack->customModuleIds.size() || voice.rack->customModuleIds[index] != SCE_NGS2_CUSTOM_MODULE_ID_FILTER) APS5_INVALID_ARG_EX; + const auto& custom = ParamAs(param); + Ngs2SamplerVoiceFilterParam filter{}; + filter.header = custom.header; + filter.index = index; + filter.location = 1; + filter.type = custom.type; + filter.channel_mask = custom.channel_mask; + if (custom.type == 1) { + filter.frequency = custom.param.fcq.frequency; + filter.q = custom.param.fcq.q; + filter.level = custom.param.fcq.level; + } else { + throw std::runtime_error("NGS2: custom filter type " + std::to_string(custom.type) + " is not implemented"); + } + Ngs2SetFilter(voice, filter); + return; + } if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + } SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); return; } + if (rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); } void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { - if (voice.userFx.empty()) return; + if (voice.userFx.empty() || std::none_of(voice.rack->customModuleIds.begin(), voice.rack->customModuleIds.end(), + [](std::uint32_t id) { return id == SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; })) return; std::vector channels(voice.channels); for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; for (std::size_t m = 0; m < voice.userFx.size(); m++) { + if (voice.rack->customModuleIds[m] != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) continue; auto& fx = voice.userFx[m]; const auto& module = voice.rack->userFx[m]; Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 7f78dc570d..8dbe429dc9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -156,6 +156,7 @@ struct Ngs2Rack { std::vector voices; std::vector userFx; std::vector> userFxCommon; + std::vector customModuleIds; }; struct Ngs2System { @@ -185,6 +186,7 @@ void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std:: int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo); int Ngs2ReleaseBuffer(const Ngs2BufferAllocator& allocator, Ngs2ContextBufferInfo bufferInfo, Ngs2ContextBufferInfo* outBufferInfo); void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); void Ngs2ConsumeAtrac9(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 1acc40d6b6..a9df1495ce 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -17,6 +17,7 @@ union RackOptions { Ngs2SubmixerRackOption submixer; Ngs2MasteringRackOption mastering; Ngs2ReverbRackOption reverb; + Ngs2CustomSamplerRackOption customSampler; Ngs2CustomSubmixerRackOption customSubmixer; }; @@ -26,13 +27,16 @@ static std::size_t RackOptionSize(std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); case SCE_NGS2_RACK_ID_REVERB: return sizeof(Ngs2ReverbRackOption); + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: return sizeof(Ngs2CustomSamplerRackOption); case SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER: return sizeof(Ngs2CustomSubmixerRackOption); default: throw std::runtime_error("NGS2: rack id " + Ngs2Hex(rackId) + " is not implemented"); } } static RackOptions DefaultRackOption(std::uint32_t rackId) { - if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: default custom rack options are not implemented"); + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER || rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + throw std::runtime_error("NGS2: default custom rack options are not implemented"); + } RackOptions options{}; auto& common = options.common; common.size = RackOptionSize(rackId); @@ -93,6 +97,7 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2CheckCustomRack(options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_REVERB && options.reverb.reverb_size != 1) { throw std::runtime_error("NGS2: reverb rack size " + std::to_string(options.reverb.reverb_size) + " is not implemented"); @@ -109,12 +114,15 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->system = system; rack->rackId = rackId; rack->maxChannels = RackMaxChannels(rackId, options); - rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0; + rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER ? options.customSampler.custom_rack_option.num_modules + : rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER ? options.customSubmixer.custom_rack_option.num_modules : 0; rack->uid = nextUid++; std::memcpy(rack->name, options.common.name, sizeof(rack->name)); rack->name[sizeof(rack->name) - 1] = '\0'; rack->maxGrainSamples = options.common.max_grain_samples; if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) rack->maxChannelWorks = options.customSampler.max_channel_works; if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) rack->maxInputs = options.customSubmixer.max_inputs; rack->bufferInfo = bufferInfo; @@ -125,6 +133,7 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO voice.ports.resize(options.common.max_ports); voice.matrices.resize(options.common.max_matrices); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) Ngs2SetupUserFx(*rack, options.customSampler.custom_rack_option); if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2SetupUserFx(*rack, options.customSubmixer.custom_rack_option); system->racks.push_back(rack); *handle = reinterpret_cast(rack); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index 6cf69f7144..bf14563e82 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -242,8 +242,11 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); voice.hasSamples = false; if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { - if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); - else MixInputs(voice, voices, grain, systemRate); + if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || voice.rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + RenderSampler(voice, grain, systemRate); + } else { + MixInputs(voice, voices, grain, systemRate); + } if (voice.rack->rackId == SCE_NGS2_RACK_ID_REVERB && voice.reverb) voice.hasSamples = Ngs2ProcessReverb(voice, grain, systemRate); Ngs2ProcessLegacyUserFx(voice, grain, systemRate); if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index b6dfb53a3d..0b49d01cb3 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -116,7 +116,14 @@ static void Patch(Ngs2Voice& voice, const Ngs2VoicePatchParam& patch) { return; } auto* dest = Ngs2FindVoice(patch.dest_handle); - if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) APS5_INVALID_ARG_EX; + if (dest == nullptr) { + auto* rack = Ngs2FindRack(patch.dest_handle); + if (rack != nullptr && !rack->voices.empty()) dest = &rack->voices.front(); + } + if (dest == nullptr || dest->rack->system != voice.rack->system || dest->rack->rackId == SCE_NGS2_RACK_ID_SAMPLER || + dest->rack->rackId == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER) { + APS5_INVALID_ARG_EX; + } port.dest = dest; } @@ -245,7 +252,7 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl } } -static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { +void Ngs2SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) { if (param.index >= voice.rack->maxFilters) APS5_INVALID_ARG_EX; if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1); auto& filter = voice.filters[param.index]; @@ -281,20 +288,28 @@ static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { switch (param.id) { - case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: SetupSampler(voice, ParamAs(param).format); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: AddWaveformBlocks(voice, ParamAs(param)); break; - case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP: + SetupSampler(voice, ParamAs(param).format); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS: + AddWaveformBlocks(voice, ParamAs(param)); + break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP: { const auto looping = std::find_if(voice.blocks.begin(), voice.blocks.end(), [](const Ngs2Block& block) { return block.info.num_repeats != 0; }); if (looping != voice.blocks.end()) looping->info.num_repeats = 0; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: { + case SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH: + case SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH: { const float ratio = ParamAs(param).ratio; if (!std::isfinite(ratio) || ratio < 0.0f) APS5_INVALID_ARG_EX; voice.pitch = ratio; break; } - case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: SetFilter(voice, ParamAs(param)); break; + case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: Ngs2SetFilter(voice, ParamAs(param)); break; default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented"); } } @@ -317,7 +332,8 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { } if (rackId != voice.rack->rackId) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); switch (rackId) { - case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; + case SCE_NGS2_RACK_ID_SAMPLER: + case SCE_NGS2_RACK_ID_CUSTOM_SAMPLER: ApplySamplerParam(voice, param); return; case SCE_NGS2_RACK_ID_SUBMIXER: if (param.id == SCE_NGS2_SUBMIXER_VOICE_PARAM_USER_FX) { const auto& fx = ParamAs(param); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index b5872f457e..c810add593 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -90,6 +90,17 @@ static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) { return false; } +template +static bool CreateSamplerThrows(const Ngs2CustomSamplerRackOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query); + } catch (const TException&) { + return true; + } + return false; +} + template static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) { param.header = {static_cast(sizeof(TParam)), 0, id}; @@ -224,8 +235,114 @@ static void TestRackOptions() { Require(calls.cleanupVoices == (std::vector{0, 1})); } +static void TestCustomSampler() { + calls = {}; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomUserFx2ModuleOption modules[2] = {Module(Gain), Module(Offset)}; + + Ngs2CustomSamplerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 2; + for (std::uint32_t m = 0; m < 2; m++) { + option.custom_rack_option.module[m].option = &modules[m].custom_module_option; + option.custom_rack_option.module[m].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; + option.custom_rack_option.module[m].state_size = 16; + } + option.max_channel_works = 4; + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + Ngs2RackInfo rackInfo{}; + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_OK); + Require(rackInfo.rack_id == SCE_NGS2_RACK_ID_CUSTOM_SAMPLER && rackInfo.type == 4 && rackInfo.max_channel_works == 4); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + std::vector pcm(Grain * 2 * 4, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 1, 0, static_cast(pcm.size() / 2), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 1.0f}); + const Ngs2VoiceParamHeader exitLoop{sizeof(Ngs2VoiceParamHeader), 0, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_EXIT_LOOP}; + Require(sceNgs2VoiceControl(voice, &exitLoop) == SCE_NGS2_OK); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + const auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestCustomFilter() { + const auto system = CreateSystem(); + const auto master = Mastering(system, 1); + + Ngs2CustomSamplerRackOption option{}; + option.custom_rack_option.rack_option.size = sizeof(option); + option.custom_rack_option.rack_option.max_grain_samples = 512; + option.custom_rack_option.rack_option.max_voices = 1; + option.custom_rack_option.rack_option.max_matrices = 1; + option.custom_rack_option.rack_option.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = 1; + option.custom_rack_option.module[0].module_id = SCE_NGS2_CUSTOM_MODULE_ID_FILTER; + option.max_channel_works = 1; + + auto invalid = option; + invalid.custom_rack_option.module[0].state_size = 4; + Require(CreateSamplerThrows(invalid)); + invalid = option; + invalid.custom_rack_option.module[0].option = reinterpret_cast(1); + Require(CreateSamplerThrows(invalid)); + + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SAMPLER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + + const auto voice = Voice(rack); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const std::vector pcm(Grain, 16384); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + + Ngs2CustomVoiceFilterParam filter{}; + filter.type = 1; + filter.param.fcq.frequency = 10000.0f; + filter.param.fcq.q = 0.70710678f; + filter.param.fcq.level = 1.0f; + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, filter); + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER | 1, filter)); + Patch(voice, master); + Event(voice, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector out(Grain, -1.0f); + const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + Require(out[0] > 0.0f && out[0] < 0.2f && out.back() > out[0]); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestUserFxChain(); TestRackOptions(); + TestCustomSampler(); + TestCustomFilter(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index eac7d3fbd9..e9ae7428fa 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -147,6 +147,27 @@ static void TestPcmBlockEnd() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestPatchToRackHandle() { + const auto system = CreateSystem(); + const auto masteringRack = CreateRack(system, SCE_NGS2_RACK_ID_MASTERING); + const auto master = Voice(masteringRack); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); + Control(master, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 0, 0}); + Event(master, SCE_NGS2_VOICE_EVENT_PLAY); + + const std::vector pcm(Grain, 16384); + const auto sampler = Sampler(system, pcm, 0); + Control(sampler, SCE_NGS2_VOICE_PARAM_PATCH, Ngs2VoicePatchParam{{}, 0, 0, masteringRack}); + + Ngs2VoicePortInfo port{}; + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &port, sizeof(port)) == SCE_NGS2_OK); + Require(port.dest_handle == master && port.dest_input_id == 0); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto out = RenderI16(system); + Require(out == pcm); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static bool Near(float a, float b) { return std::fabs(a - b) < 1e-4f; } @@ -887,6 +908,7 @@ int main() { Require(std::atexit(RenderAfterStaticTeardown) == 0); TestErrorsAndInfo(); TestPcmBlockEnd(); + TestPatchToRackHandle(); TestPan(); TestPitchAndRepeat(); TestSubmixerMatrix(); diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index 94a17e4603..86734f9a5a 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -12,6 +12,7 @@ static constexpr int ORBIS_NP_AUTH_ERROR_ABORTED = static_cast(0x80550304); static constexpr int ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550306); static constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ID = static_cast(0x80550307); static constexpr int NP_POLL_ASYNC_FINISHED = 0; +static constexpr int NP_POLL_ASYNC_RUNNING = 1; namespace { @@ -19,6 +20,7 @@ struct RequestInfo { bool isAsync = false; bool processed = false; bool aborted = false; + bool polled = false; int result = SCE_NP_ERROR_SIGNED_OUT; }; @@ -29,7 +31,7 @@ std::atomic g_nextRequest{1}; int CreateRequest(bool isAsync) { const int reqId = g_nextRequest.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_requestMutex); - g_requests[reqId] = {isAsync, false, false, SCE_NP_ERROR_SIGNED_OUT}; + g_requests[reqId] = {isAsync, false, false, false, SCE_NP_ERROR_SIGNED_OUT}; return reqId; } @@ -106,6 +108,10 @@ int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { auto it = g_requests.find(req_id); if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; if (!it->second.isAsync || (!it->second.processed && !it->second.aborted)) return ORBIS_NP_AUTH_ERROR_INVALID_ID; + if (!it->second.polled) { + it->second.polled = true; + return NP_POLL_ASYNC_RUNNING; + } if (result) *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 6bffefa394..f36474b481 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -198,8 +198,9 @@ int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { return 0; } -int APS5_VABI sceNpWebApi2SetRequestTimeout() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpWebApi2SetRequestTimeout(int64_t request_id, uint32_t timeout) { + (void)request_id; + (void)timeout; return 0; } diff --git a/core/libs/prx/libSceOpusDec/Export.cpp b/core/libs/prx/libSceOpusDec/Export.cpp index 3e1729acb6..383c6e3117 100644 --- a/core/libs/prx/libSceOpusDec/Export.cpp +++ b/core/libs/prx/libSceOpusDec/Export.cpp @@ -8,6 +8,7 @@ #include #include #include +#include extern "C" { #include @@ -19,6 +20,7 @@ struct Decoder { std::uint32_t context; int channels; AVCodecContext* codec = nullptr; + std::vector pending; ~Decoder() { avcodec_free_context(&codec); } }; @@ -115,17 +117,22 @@ int APS5_VABI sceOpusDecDecode(void* state, const std::uint8_t* data, int bytes, if (avcodec_send_packet(decoder.codec, packet.get()) < 0 || avcodec_receive_frame(decoder.codec, frame.get()) < 0) throw std::runtime_error("OpusDec: FFmpeg rejected the packet"); const auto format = static_cast(frame->format); - if (frame->nb_samples != samples || frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || + if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) throw std::runtime_error("OpusDec: unexpected FFmpeg PCM format"); - for (int sample = 0; sample < samples; ++sample) { + for (int sample = 0; sample < frame->nb_samples; ++sample) { for (int channel = 0; channel < decoder.channels; ++channel) { const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - const float value = source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]; - const auto converted = static_cast(std::lrint(std::clamp(value * 32768.0f, -32768.0f, 32767.0f))); - std::memcpy(reinterpret_cast(pcm) + (sample * decoder.channels + channel) * 2, &converted, 2); + decoder.pending.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); } } + const auto count = static_cast(samples) * decoder.channels; + if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); + for (std::size_t index = 0; index < count; ++index) { + const auto converted = static_cast(std::lrint(std::clamp(decoder.pending[index] * 32768.0f, -32768.0f, 32767.0f))); + std::memcpy(reinterpret_cast(pcm) + index * 2, &converted, 2); + } + decoder.pending.erase(decoder.pending.begin(), decoder.pending.begin() + static_cast(count)); return outputBytes; } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index db18fe77d6..bc8838e917 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -319,4 +319,10 @@ int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { return 0; } +APS5_EXPORT("zBsgF0q8DIM", scePadUnknown_zBsgF0q8DIM); +int APS5_VABI scePadUnknown_zBsgF0q8DIM() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index 21cccc0aed..a8914303d5 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -11,7 +11,6 @@ namespace Pad { -inline constexpr int MousePollIntervalMs = 33; inline constexpr int WheelPressDurationMs = 80; inline constexpr double DefaultMouseSensitivity = 1.0; inline constexpr double MaxMouseSensitivity = 10.0; @@ -21,6 +20,12 @@ struct InputSettings { int leftStickDeadzonePercent = 0; int rightStickDeadzonePercent = 0; double mouseSensitivity = DefaultMouseSensitivity; + double mouseVerticalScale = 1.0; + double mouseCurve = 1.0; + double mouseDeadzoneOffset = 0.2; + double mouseMaxSpeed = 1.0; + double mouseSmoothingMs = 10.0; + double mouseDecayMs = 30.0; }; struct InputConfiguration { diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp new file mode 100644 index 0000000000..68e501a384 --- /dev/null +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP +#define CORE_LIBS_PRX_LIBSCEPAD_KEYBOARDMOUSE_HPP + +#include +#include +#include +#include +#include "SDL_events.h" + +#include "InputMapping.hpp" +#include "PadState.hpp" + +namespace Pad { + +inline constexpr double MouseFullTiltCountsPerSecond = 3000.0; +inline constexpr double MouseMinimumStepSeconds = 0.0005; + +class MouseStick { +public: + explicit MouseStick(const InputSettings& settings); + void Move(int deltaX, int deltaY); + void Advance(double seconds); + void Reset(); + double Speed() const; + std::array Deflection() const; + +private: + InputSettings settings; + double pendingX = 0.0; + double pendingY = 0.0; + double pendingSeconds = 0.0; + double speedX = 0.0; + double speedY = 0.0; + double idleSeconds = 0.0; +}; + +struct InputRequests { + bool toggleFullscreen = false; + bool toggleMouse = false; + bool releaseMouse = false; +}; + +class KeyboardMouse { +public: + using Clock = std::chrono::steady_clock; + + explicit KeyboardMouse(InputConfiguration configuration); + InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now); + bool Advance(Clock::time_point now); + void SetMouseMode(bool enabled); + void Release(); + bool MouseMode() const; + PadInputState Compose(const PadInputState& controller) const; + const InputConfiguration& Configuration() const; + +private: + InputConfiguration configuration; + std::vector pressed; + std::vector wheelReleaseTimes; + MouseStick mouse; + Clock::time_point lastAdvance{}; + bool advanced = false; + bool mouseMode = false; +}; + +std::uint8_t StickByte(double deflection); + +} + +#endif diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index bf70cc09d6..64420b32d3 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -3,10 +3,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include #include +#include #include // The game is fully installed on the host, so every chunk is local and nothing is pending. @@ -28,27 +30,42 @@ static constexpr int SCE_PLAYGO_ERROR_BAD_LOCUS = static_cast(0x80B20010); struct ChunkTable { std::set ids{0}; bool defined = false; + bool scenario = false; }; static bool ReadPackageChunks(ChunkTable& table) { constexpr std::size_t CountOffset = 0x0A; std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); char header[CountOffset + sizeof(uint16_t)]{}; - if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) return false; + 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(); uint16_t count = 0; std::memcpy(&count, header + CountOffset, sizeof(count)); - if (count == 0) return false; + if (count == 0) throw unreadable(); for (uint32_t id = 0; id < count; ++id) table.ids.insert(static_cast(id)); table.defined = true; return true; } +static void RejectUnreadablePlayGoFiles() { + std::error_code error; + const std::filesystem::path directory = ResolvePath_nid_no_patch("/app0/sce_sys"); + for (const auto& entry : std::filesystem::directory_iterator(directory, error)) { + const auto name = entry.path().filename().string(); + if (name.rfind("playgo-", 0) == 0) + throw std::runtime_error("scePlayGo: sce_sys/" + name + " is present but no chunk table (playgo-chunk.dat) or scenario file (playgo-scenario.json) is, so the chunk ids are unknown"); + } +} + static const ChunkTable& Chunks() { static const ChunkTable chunks = [] { ChunkTable table; std::ifstream file(ResolvePath_nid_no_patch("/app0/playgo-chunkdefs.xml")); if (!file) { - ReadPackageChunks(table); + if (ReadPackageChunks(table)) return table; + table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); + if (!table.scenario) RejectUnreadablePlayGoFiles(); return table; } table.defined = true; @@ -70,7 +87,7 @@ static const ChunkTable& Chunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - return !chunks.defined || chunks.ids.contains(id); + return chunks.defined ? chunks.ids.contains(id) : chunks.scenario || id == 0; } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index fddddc543d..036eb0577d 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -36,4 +36,13 @@ if(BUILD_TESTING) set_tests_properties(guest_resolver PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_service_database_tests ${LIBS_INCLUDE_DIR}/tests/GuestServiceDatabase.cpp) + target_include_directories(guest_service_database_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_service_database_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_service_database_tests) + add_test(NAME guest_service_database COMMAND guest_service_database_tests) + if(WIN32) + set_tests_properties(guest_service_database PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp index 7ed54e7160..5e6fc068cc 100644 --- a/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp +++ b/core/libs/prx/libScePosixForWebKit/GuestResolver.hpp @@ -28,4 +28,15 @@ struct AddressInfo { static_assert(sizeof(AddressInfo) == 48); static_assert(offsetof(AddressInfo, canonicalName) == 24); static_assert(offsetof(AddressInfo, address) == 32); + +struct ServiceEntry { + char* name; + char** aliases; + std::int32_t port; + char* protocol; +}; +static_assert(sizeof(ServiceEntry) == 32); +static_assert(offsetof(ServiceEntry, aliases) == 8); +static_assert(offsetof(ServiceEntry, port) == 16); +static_assert(offsetof(ServiceEntry, protocol) == 24); } diff --git a/core/libs/prx/libScePosixForWebKit/Resolver.cpp b/core/libs/prx/libScePosixForWebKit/Resolver.cpp index 6441693f4b..0dd6901832 100644 --- a/core/libs/prx/libScePosixForWebKit/Resolver.cpp +++ b/core/libs/prx/libScePosixForWebKit/Resolver.cpp @@ -10,12 +10,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include +#include #include #include #include #include #include #include +#include #include #include "GuestResolver.hpp" @@ -31,6 +33,66 @@ struct HostLookup { std::vector addresses; }; +struct ServiceRecord { + const char* name; + std::uint16_t port; + const char* protocol; + std::array aliases; +}; + +constexpr ServiceRecord Services[]{ + {"ftp-data", 20, "tcp", {}}, {"ftp-data", 20, "udp", {}}, {"ftp-data", 20, "sctp", {}}, + {"ftp", 21, "tcp", {}}, {"ftp", 21, "udp", {}}, {"ftp", 21, "sctp", {}}, + {"ssh", 22, "tcp", {}}, {"ssh", 22, "udp", {}}, {"ssh", 22, "sctp", {}}, + {"telnet", 23, "tcp", {}}, {"telnet", 23, "udp", {}}, + {"smtp", 25, "tcp", {"mail"}}, {"smtp", 25, "udp", {"mail"}}, + {"domain", 53, "tcp", {}}, {"domain", 53, "udp", {}}, + {"tftp", 69, "tcp", {}}, {"tftp", 69, "udp", {}}, + {"http", 80, "tcp", {"www", "www-http"}}, {"http", 80, "udp", {"www", "www-http"}}, + {"http", 80, "sctp", {"www", "www-http"}}, + {"pop3", 110, "tcp", {}}, {"pop3", 110, "udp", {}}, + {"ntp", 123, "tcp", {}}, {"ntp", 123, "udp", {}}, + {"imap", 143, "tcp", {"imap2", "imap4"}}, {"imap", 143, "udp", {"imap2", "imap4"}}, + {"https", 443, "tcp", {}}, {"https", 443, "udp", {}}, {"https", 443, "sctp", {}}, + {"rtsp", 554, "tcp", {}}, {"rtsp", 554, "udp", {}}, + {"submission", 587, "tcp", {}}, {"submission", 587, "udp", {}}, + {"imaps", 993, "tcp", {}}, {"imaps", 993, "udp", {}}, + {"pop3s", 995, "tcp", {"spop3"}}, {"pop3s", 995, "udp", {"spop3"}}, +}; + +constexpr std::size_t ServiceNameSize = 11; +constexpr std::size_t ServiceAliasSize = 9; +constexpr std::size_t ServiceProtocolSize = 5; +constexpr std::size_t ServiceAliasCount = std::tuple_size_v; + +template +constexpr std::size_t LongestServiceText(TSelect select) { + std::size_t longest = 0; + for (const auto& service : Services) + for (const char* text : select(service)) + if (text) longest = std::max(longest, std::char_traits::length(text)); + return longest; +} + +static_assert(ServiceNameSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.name}; }) + 1); +static_assert(ServiceAliasSize == LongestServiceText([](const ServiceRecord& record) { return record.aliases; }) + 1); +static_assert(ServiceProtocolSize == LongestServiceText([](const ServiceRecord& record) { return std::array{record.protocol}; }) + 1); + +struct ServiceLookup { + GuestResolver::ServiceEntry entry; + std::array name; + std::array protocol; + std::array, ServiceAliasCount> aliasNames; + std::array aliases; +}; +static_assert(std::is_trivially_destructible_v); + +template +char* CopyServiceText(std::array& target, const char* text) { + std::memcpy(target.data(), text, std::strlen(text) + 1); + return target.data(); +} + bool NumericAddress(const char* name, std::array& address) { std::array parts{}; std::size_t count = 0; @@ -155,6 +217,27 @@ GuestResolver::HostEntry* APS5_VABI gethostbyname_nid_postfix(const char* name) } } +GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char* name, const char* protocol) { + for (const auto& service : Services) { + bool named = std::strcmp(name, service.name) == 0; + for (const char* alias : service.aliases) + named = named || (alias && std::strcmp(name, alias) == 0); + if (!named || (protocol && std::strcmp(protocol, service.protocol) != 0)) continue; + static thread_local ServiceLookup lookup; + std::size_t count = 0; + for (const char* alias : service.aliases) { + if (!alias) continue; + lookup.aliases[count] = CopyServiceText(lookup.aliasNames[count], alias); + ++count; + } + lookup.aliases[count] = nullptr; + lookup.entry = {CopyServiceText(lookup.name, service.name), lookup.aliases.data(), htons(service.port), + CopyServiceText(lookup.protocol, service.protocol)}; + return &lookup.entry; + } + return nullptr; +} + void APS5_VABI freeaddrinfo_nid_postfix(GuestResolver::AddressInfo* first) { while (first) { auto* next = first->next; diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 08cc546d3d..00529793b7 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/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -219,6 +220,10 @@ int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { return g_transaction_counter.fetch_add(1); } +static std::string blocks_path(const std::string& real_path) { + return real_path + ".blocks"; +} + static int deleteSave(const SaveDataDelete* del) { if (del == nullptr || del->dir_name == nullptr) { throw std::runtime_error("sceSaveDataDelete: null argument"); @@ -254,6 +259,7 @@ static int deleteSave(const SaveDataDelete* del) { } std::error_code ec; std::filesystem::remove(param_path(path), ec); + std::filesystem::remove(blocks_path(path), ec); return SAVE_DATA_OK; } @@ -268,6 +274,45 @@ int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { return SAVE_DATA_OK; } +static constexpr std::uint64_t SAVE_DATA_BLOCK_BYTES = 32768; + +static std::uint64_t save_blocks(const std::string& real_path) { + std::ifstream file(blocks_path(real_path)); + std::uint64_t blocks = 0; + if (!(file >> blocks) || blocks == 0) { + return SAVE_DATA_BLOCKS_MAX; + } + return std::min(blocks, SAVE_DATA_BLOCKS_MAX); +} + +static void keep_save_blocks(const std::string& real_path, std::uint64_t blocks) { + std::ofstream file(blocks_path(real_path), std::ios::trunc); + file << std::min(blocks, SAVE_DATA_BLOCKS_MAX) << '\n'; + if (!file) { + throw std::runtime_error("sceSaveDataMount3: the save's size could not be kept"); + } +} + +static std::uint64_t used_blocks(const std::string& real_path) { + std::uint64_t blocks = 0; + std::error_code error; + for (auto it = std::filesystem::recursive_directory_iterator(real_path, error); !error && it != std::filesystem::recursive_directory_iterator(); it.increment(error)) { + if (!it->is_regular_file(error)) { + continue; + } + const auto bytes = it->file_size(error); + if (!error) { + blocks += (bytes + SAVE_DATA_BLOCK_BYTES - 1) / SAVE_DATA_BLOCK_BYTES; + } + } + return blocks; +} + +static SaveDataSearchInfo search_info(const std::string& real_path) { + const auto blocks = save_blocks(real_path); + return SaveDataSearchInfo{blocks, blocks - std::min(blocks, used_blocks(real_path)), {}}; +} + static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { if (cond == nullptr || result == nullptr) { throw std::runtime_error("sceSaveDataDirNameSearch: null argument"); @@ -278,6 +323,9 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { return SAVE_DATA_ERROR_PARAMETER; } + if (cond->title_id != nullptr && std::strncmp(cond->title_id->data, GetAppTitleId_nid_postfix().value, sizeof(cond->title_id->data)) != 0) { + return SAVE_DATA_OK; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { @@ -287,6 +335,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS std::string name; std::string path; SaveDataParam param; + SaveDataSearchInfo info; }; const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; std::vector matches; @@ -298,7 +347,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - Match match{name, entry.path().string(), {}}; + Match match{name, entry.path().string(), {}, search_info(entry.path().string())}; if (sortByParam) { match.param = load_param(match.path); } @@ -309,6 +358,10 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); } else if (key == SAVE_DATA_SORT_KEY_MTIME) { std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } else if (key == SAVE_DATA_SORT_KEY_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.blocks < b.info.blocks; }); + } else if (key == SAVE_DATA_SORT_KEY_FREE_BLOCKS) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.info.free_blocks < b.info.free_blocks; }); } if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { std::reverse(matches.begin(), matches.end()); @@ -320,7 +373,7 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } if (result->infos != nullptr) { - result->infos[i] = SaveDataSearchInfo{SAVE_DATA_BLOCKS_MAX, SAVE_DATA_BLOCKS_MAX, {}}; + result->infos[i] = matches[i].info; } } result->hit_num = static_cast(matches.size()); @@ -353,12 +406,14 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } std::lock_guard lock(g_slots_mutex); - if (find_slot_by_mount_point(mount_point->data) == -1) { + const int slot = find_slot_by_mount_point(mount_point->data); + if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } std::memset(info, 0, sizeof(*info)); - info->blocks = SAVE_DATA_BLOCKS_MAX; - info->free_blocks = SAVE_DATA_BLOCKS_MAX; + const auto search = search_info(g_slots[slot].real_path); + info->blocks = search.blocks; + info->free_blocks = search.free_blocks; return SAVE_DATA_OK; } @@ -464,7 +519,6 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result if (mount == nullptr || mount_result == nullptr || mount->dir_name == nullptr) { throw std::runtime_error("sceSaveDataMount3: null argument"); } - std::memset(mount_result, 0, sizeof(*mount_result)); const bool create = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE) != 0; const bool create2 = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_CREATE2) != 0; const bool rdonly = (mount->mount_mode & SAVE_DATA_MOUNT_MODE_RDONLY) != 0; @@ -506,12 +560,17 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result // path resolver maps it to the save directory, whose name may be far longer. const std::string mountPoint = "/savedata" + std::to_string(slot); AddPathAlias_nid_no_patch(mountPoint.c_str(), std::filesystem::absolute(real_path).string().c_str()); + std::memset(mount_result, 0, sizeof(*mount_result)); g_slots[slot].used = true; g_slots[slot].mount_point = mountPoint; g_slots[slot].real_path = real_path; std::memcpy(mount_result->mount_point.data, mountPoint.c_str(), mountPoint.size() + 1); mount_result->required_blocks = 0; mount_result->mount_status = (create || (create2 && !exists)) ? 1u : 0u; + std::error_code sized; + if (mount->blocks != 0 && (create || (create2 && !exists) || !std::filesystem::exists(blocks_path(real_path), sized))) { + keep_save_blocks(real_path, mount->blocks); + } return SAVE_DATA_OK; } diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index d66b9716ec..8142808e06 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -1,5 +1,68 @@ +function(configure_ssl_backend) + set(options ENABLE_PROGRAMS ENABLE_TESTING GEN_FILES USE_STATIC_MBEDTLS_LIBRARY + USE_SHARED_MBEDTLS_LIBRARY DISABLE_PACKAGE_CONFIG_AND_INSTALL MBEDTLS_CONFIG_FILE MBEDTLS_USER_CONFIG_FILE) + foreach(option IN LISTS options) + get_property(saved_${option}_exists CACHE ${option} PROPERTY TYPE SET) + if(saved_${option}_exists) + get_property(saved_${option}_type CACHE ${option} PROPERTY TYPE) + get_property(saved_${option}_help CACHE ${option} PROPERTY HELPSTRING) + get_property(saved_${option}_value CACHE ${option} PROPERTY VALUE) + endif() + endforeach() + set(ENABLE_PROGRAMS OFF) + set(ENABLE_TESTING OFF) + set(GEN_FILES OFF) + set(USE_STATIC_MBEDTLS_LIBRARY ON) + set(USE_SHARED_MBEDTLS_LIBRARY OFF) + set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON) + set(MBEDTLS_CONFIG_FILE "") + set(MBEDTLS_USER_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/MbedConfig.hpp") + foreach(option IN LISTS options) + if(option MATCHES "CONFIG_FILE$") + set(${option} "${${option}}" CACHE FILEPATH "" FORCE) + else() + set(${option} "${${option}}" CACHE BOOL "" FORCE) + endif() + endforeach() + if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM OR CMAKE_POLICY_VERSION_MINIMUM VERSION_LESS 3.5) + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + endif() + add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/mbedtls ${CMAKE_BINARY_DIR}/3rdparty/mbedtls EXCLUDE_FROM_ALL) + foreach(property LINK_LIBRARIES INTERFACE_LINK_LIBRARIES) + get_target_property(libraries mbedcrypto ${property}) + if(libraries) + list(REMOVE_ITEM libraries everest) + set_property(TARGET mbedcrypto PROPERTY ${property} "${libraries}") + endif() + endforeach() + set_target_properties(everest PROPERTIES EXCLUDE_FROM_ALL ON) + foreach(option IN LISTS options) + if(saved_${option}_exists) + set(${option} "${saved_${option}_value}" CACHE ${saved_${option}_type} "${saved_${option}_help}" FORCE) + else() + unset(${option} CACHE) + endif() + endforeach() +endfunction() + +configure_ssl_backend() +set_target_properties(mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(MINGW) + target_compile_options(mbedcrypto PRIVATE -fno-asynchronous-unwind-tables) + target_compile_options(mbedx509 PRIVATE -fno-asynchronous-unwind-tables) + target_compile_options(p256m PRIVATE -fno-asynchronous-unwind-tables) +endif() + add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceSsl PRIVATE mbedx509 mbedcrypto) +set_target_properties(libSceSsl PROPERTIES VISIBILITY_INLINES_HIDDEN ON) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceSsl PRIVATE LINKER:--exclude-libs,ALL) +endif() +if(WIN32) + target_link_libraries(libSceSsl PRIVATE bcrypt ws2_32) +endif() configure_windows_unwind(libSceSsl) if(BUILD_TESTING) @@ -8,8 +71,13 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_tests) add_test(NAME guest_ssl COMMAND guest_ssl_tests) + add_test_executable(guest_ssl_load_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslLoad.cpp) + target_include_directories(guest_ssl_load_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_load_tests PRIVATE libSceSsl libc) + configure_windows_unwind(guest_ssl_load_tests) + add_test(NAME guest_ssl_load COMMAND guest_ssl_load_tests) if(WIN32) - set_tests_properties(guest_ssl PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_ssl guest_ssl_load PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index fb2265f22d..b12d6bb913 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,99 +1,460 @@ -#include -#include -#include -#include "SceTypes.hpp" +#include "SslTypes.hpp" #include "prx/libc/include/General.hpp" -#include -#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include #include #include #include - -// No network is emulated: contexts, templates and requests can be created, but any request -// that would touch the network fails with the library's network error. -static constexpr int ERROR_NETWORK = static_cast(0x80435001); -static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +#include +#include +#include namespace { -constexpr int ERROR_NOT_FOUND = static_cast(0x8095F004); -constexpr int ERROR_INVALID_ARG = static_cast(0x8095177A); -constexpr int ERROR_INVALID_ID = static_cast(0x8095F006); -constexpr int ERROR_INVALID_VALUE = static_cast(0x8095F007); +constexpr int ErrorNotFound = static_cast(0x8095f004); +constexpr int ErrorInvalidArg = static_cast(0x8095177a); +constexpr int ErrorInvalidId = static_cast(0x8095f006); -struct SslData { - char* ptr; - size_t size; +struct Pool { + std::size_t size, current = 0, peak = 0; + bool exhausted = false; }; -struct SslMemoryPoolStats { - size_t pool_size; - size_t max_inuse_size; - size_t current_inuse_size; - int32_t reserved; +struct alignas(std::max_align_t) Allocation { + Pool* pool; + std::size_t size; }; -struct SslCaCerts { - SslData* certs; - size_t num; - void* pool; +thread_local Pool* currentPool = nullptr; + +void* PoolCalloc(std::size_t count, std::size_t size) noexcept { + if (!currentPool) return nullptr; + if (size && count > std::numeric_limits::max() / size) { + currentPool->exhausted = true; + return nullptr; + } + const auto bytes = count * size; + if (bytes > currentPool->size - currentPool->current || bytes > std::numeric_limits::max() - sizeof(Allocation)) { + currentPool->exhausted = true; + return nullptr; + } + auto* allocation = static_cast(std::calloc(1, sizeof(Allocation) + bytes)); + if (!allocation) { + currentPool->exhausted = true; + return nullptr; + } + allocation->pool = currentPool; + allocation->size = bytes; + currentPool->current += bytes; + currentPool->peak = std::max(currentPool->peak, currentPool->current); + return allocation + 1; +} + +void PoolFree(void* pointer) noexcept { + if (!pointer) return; + auto* allocation = static_cast(pointer) - 1; + allocation->pool->current -= allocation->size; + mbedtls_platform_zeroize(pointer, allocation->size); + std::free(allocation); +} + +struct PoolScope { + Pool* previous; + explicit PoolScope(Pool& pool) : previous(std::exchange(currentPool, &pool)) { pool.exhausted = false; } + ~PoolScope() { currentPool = previous; } }; -struct SslCaList { - void** certs; - int num; +struct PoolDeleter { + void operator()(std::uint8_t* bytes) const { PoolFree(bytes); } }; -static_assert(sizeof(SslCaList) == 16); -void RequireContext(const char* function, int sslCtxId) { - std::lock_guard lock(g_poolsMutex); - if (!g_pools.contains(sslCtxId)) throw std::invalid_argument(std::string(function) + ": unknown context"); +using Block = std::unique_ptr; + +Block Allocate(std::size_t size) { + Block block(static_cast(PoolCalloc(1, std::max(1, size)))); + if (!block) throw std::bad_alloc(); + return block; +} + +std::size_t Add(std::size_t left, std::size_t right) { + if (right > std::numeric_limits::max() - left) throw std::length_error("SSL allocation size overflow"); + return left + right; +} + +void CheckBackend(int error, const char* message) { + if (error == 0) return; + if (currentPool && currentPool->exhausted) throw std::bad_alloc(); + throw std::invalid_argument(message); +} + +struct CertificateChain; + +struct CertificateHandle { + std::weak_ptr owner; + const mbedtls_x509_crt* certificate; +}; + +struct CertificateChain { + mbedtls_x509_crt root; + std::vector handles; + CertificateChain() { mbedtls_x509_crt_init(&root); } + ~CertificateChain() { mbedtls_x509_crt_free(&root); } +}; + +struct PrivateKey { + mbedtls_pk_context key; + PrivateKey() { mbedtls_pk_init(&key); } + ~PrivateKey() { mbedtls_pk_free(&key); } +}; + +struct Random { + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + Random() { + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + } + ~Random() { + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + } + void Seed() { + CheckBackend(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0), "SSL random generator initialization failed"); + } +}; + +struct Identity { + std::shared_ptr chain; + PrivateKey key; +}; + +struct Snapshot { + Block data; + std::vector> certificates; + std::size_t count; +}; + +struct Context { + Pool pool; + std::vector> authorities; + std::shared_ptr identity; + std::vector> certificateHandles; + std::unordered_map caLists; + std::unordered_map caCerts; + explicit Context(std::size_t size) : pool{size} {} +}; + +using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); + +struct SslConnection { + int context; + int socket; + std::string host; + SslVerifyCallback verifyCallback; + void* verifyArgument; +}; + +struct Runtime { + std::mutex mutex; + std::unordered_map> contexts; + std::unordered_map connections; + unsigned nextHandle = 1; + Runtime() { + if (mbedtls_platform_set_calloc_free(PoolCalloc, PoolFree) != 0) + throw std::runtime_error("SSL allocator initialization failed"); + } +}; + +Runtime& State() { + static auto* runtime = new Runtime; + return *runtime; +} + +Context& GetContext(int handle) { + const auto found = State().contexts.find(handle); + if (found == State().contexts.end()) throw std::invalid_argument("Invalid SSL context"); + return *found->second; +} + +SslConnection& RequireConnection(const char* function, 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; +} + +struct Input { + Block bytes; + std::size_t size; +}; + +Input CopyInput(const SceSsl::Data& data) { + if (!data.ptr || !data.size) throw std::invalid_argument("Empty SSL certificate/key data"); + auto bytes = Allocate(Add(data.size, 1)); + std::memcpy(bytes.get(), data.ptr, data.size); + auto size = data.size; + std::size_t first = 0; + while (first < size && (bytes.get()[first] == ' ' || bytes.get()[first] == '\r' || bytes.get()[first] == '\n' || bytes.get()[first] == '\t')) ++first; + const bool pem = first < size && bytes.get()[first] == '-'; + if (pem) { + if (bytes.get()[size - 1] != 0) ++size; + if (std::memchr(bytes.get(), 0, size - 1)) throw std::invalid_argument("Embedded null in SSL PEM data"); + } else { + auto* position = bytes.get(); + std::size_t length = 0; + CheckBackend(mbedtls_asn1_get_tag(&position, bytes.get() + size, &length, MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE), + "Invalid SSL DER sequence"); + if (length != static_cast(bytes.get() + size - position)) throw std::invalid_argument("Trailing SSL DER data"); + } + return {std::move(bytes), size}; +} + +std::shared_ptr ParseCertificate(const SceSsl::Data& data) { + auto chain = std::make_shared(); + const auto input = CopyInput(data); + CheckBackend(mbedtls_x509_crt_parse(&chain->root, input.bytes.get(), input.size), "Invalid SSL certificate chain"); + return chain; +} + +std::size_t CertificateCount(const Context& context) { + std::size_t count = 0; + for (const auto& chain : context.authorities) count = Add(count, chain->handles.size()); + return count; } } extern "C" { -int APS5_VABI sceSslFreeCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { + 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"); + const auto handle = static_cast(state.nextHandle); + state.contexts.emplace(handle, std::make_unique(poolSize)); + ++state.nextHandle; + return handle; +} + +int APS5_VABI sceSslTerm_nid_postfix(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.contexts.erase(sslCtxId); return 0; } -int APS5_VABI sceSslGetCaCerts(int ssl_ctx_id, void* ca_certs) { - (void)ssl_ctx_id; - if (!ca_certs) return ERROR_INVALID_ARG; - *static_cast(ca_certs) = {}; - return ERROR_NOT_FOUND; -} - -int APS5_VABI sceSslGetCaList(int sslCtxId, SslCaList* caList) { - if (!caList) return ERROR_INVALID_ARG; - RequireContext(__func__, sslCtxId); - *caList = {}; - return ERROR_NOT_FOUND; -} - -int APS5_VABI sceSslFreeCaList(int sslCtxId, SslCaList* caList) { - if (!caList) return ERROR_INVALID_ARG; - RequireContext(__func__, sslCtxId); - if (caList->certs || caList->num) throw std::invalid_argument("sceSslFreeCaList: list not returned by sceSslGetCaList"); +int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* stats) { + if (!stats) return static_cast(0x8095F007); + 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); + } + const auto& pool = found->second->pool; + *stats = {pool.size, pool.peak, pool.current, 0}; return 0; } -int APS5_VABI sceSslInit_nid_postfix(uint64_t pool_size) { - const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; +int APS5_VABI sceSslLoadCert(int sslCtxId, int caCertNum, SceSsl::Data** caList, SceSsl::Data* cert, SceSsl::Data* privateKey) { + if (caCertNum < 0 || (caCertNum && !caList) || (!!cert != !!privateKey)) + throw std::invalid_argument("Invalid SSL certificate load arguments"); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + std::vector> authorities; + std::vector> handles; + for (int index = 0; index < caCertNum; ++index) { + if (!caList[index]) throw std::invalid_argument("Missing SSL certificate descriptor"); + auto chain = ParseCertificate(*caList[index]); + for (auto* entry = &chain->root; entry; entry = entry->next) { + auto handle = std::make_unique(CertificateHandle{chain, entry}); + chain->handles.push_back(handle.get()); + handles.push_back(std::move(handle)); + } + authorities.push_back(std::move(chain)); + } + std::shared_ptr identity; + if (cert) { + identity = std::make_shared(); + identity->chain = ParseCertificate(*cert); + const auto key = CopyInput(*privateKey); + Random random; + random.Seed(); + CheckBackend(mbedtls_pk_parse_key(&identity->key.key, key.bytes.get(), key.size, nullptr, 0, + mbedtls_ctr_drbg_random, &random.drbg), "Invalid SSL private key"); + CheckBackend(mbedtls_pk_check_pair(&identity->chain->root.pk, &identity->key.key, mbedtls_ctr_drbg_random, &random.drbg), + "SSL client certificate and private key do not match"); + } + context.authorities.reserve(Add(context.authorities.size(), authorities.size())); + context.certificateHandles.reserve(Add(context.certificateHandles.size(), handles.size())); + for (auto& chain : authorities) context.authorities.push_back(std::move(chain)); + for (auto& handle : handles) context.certificateHandles.push_back(std::move(handle)); + if (identity) context.identity = std::move(identity); + return 0; +} + +int APS5_VABI sceSslUnloadCert(int sslCtxId) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + context.authorities.clear(); + context.identity.reset(); + return 0; +} + +int APS5_VABI sceSslGetCaList(int sslCtxId, SceSsl::CaList* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > static_cast(std::numeric_limits::max())) throw std::length_error("Too many SSL authorities"); + Snapshot snapshot{Allocate(count * sizeof(void*)), context.authorities, count}; + auto** pointers = reinterpret_cast(snapshot.data.get()); + std::size_t index = 0; + for (const auto& chain : snapshot.certificates) for (auto* handle : chain->handles) pointers[index++] = handle; + context.caLists.emplace(pointers, std::move(snapshot)); + *output = {pointers, static_cast(count)}; + return 0; +} + +int APS5_VABI sceSslFreeCaList(int sslCtxId, SceSsl::CaList* list) { + if (!list) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + if (!list->certs && list->num == 0) return 0; + const auto found = context.caLists.find(list->certs); + if (found == context.caLists.end() || list->num < 0 || static_cast(list->num) != found->second.count) + throw std::invalid_argument("Invalid SSL CA list allocation"); + context.caLists.erase(found); + *list = {}; + return 0; +} + +int APS5_VABI sceSslGetCaCerts(int sslCtxId, SceSsl::CaCerts* output) { + if (!output) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found == state.contexts.end()) { + *output = {}; + return ErrorNotFound; + } + auto& context = *found->second; + PoolScope scope(context.pool); + const auto count = CertificateCount(context); + if (!count) { + *output = {}; + return ErrorNotFound; + } + if (count > std::numeric_limits::max() / sizeof(SceSsl::Data)) throw std::length_error("Too many SSL authorities"); + auto total = count * sizeof(SceSsl::Data); + for (const auto& chain : context.authorities) + for (auto* entry = &chain->root; entry; entry = entry->next) total = Add(total, entry->raw.len); + Snapshot snapshot{Allocate(total), {}, count}; + auto* data = reinterpret_cast(snapshot.data.get()); + auto* next = snapshot.data.get() + count * sizeof(SceSsl::Data); + std::size_t index = 0; + for (const auto& chain : context.authorities) { + for (auto* entry = &chain->root; entry; entry = entry->next) { + data[index++] = {next, entry->raw.len}; + std::memcpy(next, entry->raw.p, entry->raw.len); + next += entry->raw.len; + } + } + context.caCerts.emplace(data, std::move(snapshot)); + *output = {data, count, data}; + return 0; +} + +int APS5_VABI sceSslFreeCaCerts(int sslCtxId, SceSsl::CaCerts* certificates) { + if (!certificates) return ErrorInvalidArg; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.contexts.find(sslCtxId); + if (found != state.contexts.end()) { + auto& snapshots = found->second->caCerts; + const auto allocation = snapshots.find(certificates->pool); + if (allocation != snapshots.end()) { + if (certificates->certs != certificates->pool || certificates->num != allocation->second.count) + throw std::invalid_argument("Invalid SSL CA certificate allocation"); + snapshots.erase(allocation); + } + } + *certificates = {}; + return 0; +} + +int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) { + if (!host) throw std::invalid_argument("sceSslCreateConnection: null host"); + std::lock_guard lock(State().mutex); + if (!State().contexts.contains(sslCtxId)) return ErrorInvalidId; + auto& state = State(); + if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL connection handles exhausted"); + const auto id = static_cast(state.nextHandle); + state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr}); + ++state.nextHandle; return id; } -int APS5_VABI sceSslTerm_nid_postfix(int ssl_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(ssl_ctx_id); +int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::lock_guard lock(State().mutex); + if (!State().connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + 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; + return 0; +} + +int APS5_VABI sceSslConnect(int sslConnectionId) { + (void)sslConnectionId; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { + (void)sslConnectionId; + (void)buf; + (void)len; + (void)flags; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { + (void)sslConnectionId; + (void)buf; + (void)len; + (void)flags; + NotImplemented_nid_no_patch(__func__); return 0; } @@ -107,28 +468,6 @@ int APS5_VABI sceSslGetSerialNumber() { return 0; } -int APS5_VABI sceSslLoadCert() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslUnloadCert(int sslCtxId) { - RequireContext(__func__, sslCtxId); - return 0; -} - -int APS5_VABI sceSslGetMemoryPoolStats(int ssl_ctx_id, SslMemoryPoolStats* stats) { - if (stats == nullptr) return ERROR_INVALID_VALUE; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(ssl_ctx_id); - if (pool == g_pools.end()) { - std::memset(stats, 0, sizeof(*stats)); - return ERROR_INVALID_ID; - } - *stats = {static_cast(pool->second), 0, 0, 0}; - return 0; -} - int APS5_VABI sceSslFreeSslCertName(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -179,39 +518,9 @@ int APS5_VABI sceSslGetSubjectName(void) { return 0; } -int APS5_VABI sceSslCreateConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslDeleteConnection(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceSslEnableVerifyOption(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceSslSetVerifyCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslConnect(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslRead(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceSslWrite(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp new file mode 100644 index 0000000000..ab4361c8a7 --- /dev/null +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define MBEDTLS_PLATFORM_MEMORY +#undef MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED diff --git a/core/libs/prx/libSceSsl/SslTypes.hpp b/core/libs/prx/libSceSsl/SslTypes.hpp new file mode 100644 index 0000000000..18fc70f740 --- /dev/null +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace SceSsl { + +struct Data { + std::uint8_t* ptr; + std::size_t size; +}; + +struct CaCerts { + Data* certs; + std::size_t num; + void* pool; +}; + +struct CaList { + void** certs; + int num; +}; + +struct MemoryPoolStats { + std::size_t poolSize, maxInuseSize, currentInuseSize; + std::uint32_t reserved; +}; + +static_assert(sizeof(Data) == 0x10); +static_assert(sizeof(CaCerts) == 0x18); +static_assert(sizeof(CaList) == 0x10); +static_assert(sizeof(MemoryPoolStats) == 0x20); + +} diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 9b90ef5883..3674b130bc 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -3,6 +3,17 @@ target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) + if(WIN32) + add_test_executable(guest_system_service_path_boundary_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServicePathBoundary.cpp) + target_include_directories(guest_system_service_path_boundary_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_path_boundary_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_path_boundary_tests) + add_test(NAME guest_system_service_path_boundary COMMAND guest_system_service_path_boundary_tests) + set_tests_properties(guest_system_service_path_boundary PROPERTIES TIMEOUT 20) + set_tests_properties(guest_system_service_path_boundary PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_system_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_exit_tests PRIVATE libSceSystemService libc) @@ -13,14 +24,17 @@ if(BUILD_TESTING) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) 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) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") set_tests_properties(guest_system_relaunch PROPERTIES PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) + set_tests_properties(guest_nested_exit PROPERTIES + PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9bdc3e6cac..0db2c17be3 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -21,6 +21,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" namespace { @@ -65,6 +66,12 @@ std::string ConvertedExecutableName(const char* path) { } } +void RequireExecutableBeside(const std::filesystem::path& directory, const std::filesystem::path& target, const char* guestPath) { + if (target.lexically_normal().parent_path() != directory.lexically_normal()) { + UnconvertedExecutable(std::string("an executable outside /app0 (") + guestPath + ")"); + } +} + #if defined(_WIN32) std::wstring Utf16(const std::string& text) { if (text.empty()) return {}; @@ -115,6 +122,7 @@ HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { launch.executable = mainLength > 0 && mainLength < main.size() ? main.substr(0, mainLength) : own; } else { const auto target = ownPath.parent_path() / (Utf16(ConvertedExecutableName(path)) + ownPath.extension().wstring()); + RequireExecutableBeside(ownPath.parent_path(), target, path); if (!std::filesystem::is_regular_file(target)) { UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); } @@ -156,6 +164,7 @@ HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { launch.executable = main != nullptr && *main != '\0' ? main : own.string(); } else { const auto target = own.parent_path() / (ConvertedExecutableName(path) + own.extension().string()); + RequireExecutableBeside(own.parent_path(), target, path); if (!std::filesystem::is_regular_file(target)) { UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); } @@ -217,9 +226,8 @@ int APS5_VABI sceSystemServiceGetHdrToneMapLuminance(SystemServiceHdrToneMapLumi if (luminance == nullptr) { return SYSTEM_SERVICE_ERROR_PARAMETER; } - constexpr float SdrReferenceWhiteNits = 100.0f; - luminance->max_full_frame_tone_map_luminance = SdrReferenceWhiteNits; - luminance->max_tone_map_luminance = SdrReferenceWhiteNits; + luminance->max_full_frame_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; + luminance->max_tone_map_luminance = AgcDriver::SdrToneMapLuminanceNits; luminance->min_tone_map_luminance = 0.0f; return SYSTEM_SERVICE_OK; } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 6b9a38c833..0bfa4cdd06 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -8,7 +8,9 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/PadInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MouseInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ImeTextInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/InputMapping.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/KeyboardMouse.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp ) @@ -17,7 +19,7 @@ target_include_directories(libSceVideoOut PRIVATE ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ) -target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libc libkernel decoder_png SDL2-static) +target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libScePad libSceMouse libSceKeyboard libSceIme libc libkernel decoder_png SDL2-static) if(WIN32) target_link_libraries(libSceVideoOut PRIVATE comctl32) endif() @@ -112,6 +114,17 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING) + add_test_executable(pad_keyboard_mouse_tests tests/KeyboardMouse.cpp src/KeyboardMouse.cpp) + target_include_directories(pad_keyboard_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) + target_link_libraries(pad_keyboard_mouse_tests PRIVATE SDL2-static) + if(MINGW) + target_compile_options(pad_keyboard_mouse_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(pad_keyboard_mouse_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_test(NAME pad_keyboard_mouse COMMAND pad_keyboard_mouse_tests) +endif() + if(BUILD_TESTING) add_test_executable(guest_video_out_wqhd_detection_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutWqhdDetection.cpp) target_include_directories(guest_video_out_wqhd_detection_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 0c946149c5..ef58c55add 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -17,11 +17,6 @@ int APS5_VABI sceVideoOutClearFlipMaster() { return 0; } -int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); return sceVideoOutAdjustColor(handle, settings); diff --git a/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp new file mode 100644 index 0000000000..81cfe59fc1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/include/ImeTextInput.hpp @@ -0,0 +1,11 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_IMETEXTINPUT_HPP + +#include "SDL_events.h" + +class ImeTextInput { +public: + void HandleEvent(const SDL_Event& event, unsigned windowId); +}; + +#endif diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index b68b5e5cd3..de1c799e0e 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -3,11 +3,9 @@ #include "SDL_events.h" #include "SDL_gamecontroller.h" -#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" #include "prx/libScePad/include/PadState.hpp" -#include #include -#include class DisplayWindow; @@ -17,7 +15,7 @@ public: ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); - bool MouseModeEnabled() const { return mouseEnabled; } + bool MouseModeEnabled() const { return keyboardMouse.MouseMode(); } private: explicit PadInput(Pad::InputConfiguration configuration); @@ -30,13 +28,7 @@ private: void applyOutput(); void enableSensors(); - std::vector bindings; - Pad::InputSettings settings; - std::vector pressed; - std::vector wheelReleaseTimes; - std::array mouseStick{128, 128}; - std::chrono::steady_clock::time_point nextMousePoll{}; - bool mouseEnabled = false; + Pad::KeyboardMouse keyboardMouse; SDL_GameController* controller = nullptr; PadInputState controllerState{}; std::uint32_t outputSequence = 0; diff --git a/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp new file mode 100644 index 0000000000..26ddb4b216 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/ImeTextInput.cpp @@ -0,0 +1,42 @@ +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" + +#include + +#include "SDL_keyboard.h" + +void ImeTextInput::HandleEvent(const SDL_Event& event, unsigned windowId) { + ImeHostInput input{}; + switch (event.type) { + case SDL_TEXTINPUT: + if (event.text.windowID != windowId) return; + static_assert(sizeof(input.text) >= sizeof(event.text.text)); + std::memcpy(input.text, event.text.text, sizeof(event.text.text)); + input.kind = ImeHostInputKind::Text; + break; + case SDL_KEYDOWN: + if (event.key.windowID != windowId) return; + switch (event.key.keysym.scancode) { + case SDL_SCANCODE_BACKSPACE: input.kind = ImeHostInputKind::Backspace; break; + case SDL_SCANCODE_DELETE: input.kind = ImeHostInputKind::Delete; break; + case SDL_SCANCODE_LEFT: input.kind = ImeHostInputKind::Left; break; + case SDL_SCANCODE_RIGHT: input.kind = ImeHostInputKind::Right; break; + case SDL_SCANCODE_HOME: input.kind = ImeHostInputKind::Home; break; + case SDL_SCANCODE_END: input.kind = ImeHostInputKind::End; break; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Enter; + break; + case SDL_SCANCODE_ESCAPE: + if (event.key.repeat != 0) return; + input.kind = ImeHostInputKind::Escape; + break; + default: return; + } + break; + default: + return; + } + ImePublishInput_nid_postfix(input); +} diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index b407bfeb2e..d974db9e75 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -126,7 +126,13 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc enum class InputSetting { LeftStickDeadzone, RightStickDeadzone, - MouseSensitivity + MouseSensitivity, + MouseVerticalScale, + MouseCurve, + MouseDeadzoneOffset, + MouseMaxSpeed, + MouseSmoothingMs, + MouseDecayMs }; struct InputSettingName { @@ -137,7 +143,13 @@ struct InputSettingName { constexpr auto settingNames = std::array{ InputSettingName{"LeftStickDeadzone", InputSetting::LeftStickDeadzone}, InputSettingName{"RightStickDeadzone", InputSetting::RightStickDeadzone}, - InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity} + InputSettingName{"MouseSensitivity", InputSetting::MouseSensitivity}, + InputSettingName{"MouseVerticalScale", InputSetting::MouseVerticalScale}, + InputSettingName{"MouseCurve", InputSetting::MouseCurve}, + InputSettingName{"MouseDeadzoneOffset", InputSetting::MouseDeadzoneOffset}, + InputSettingName{"MouseMaxSpeed", InputSetting::MouseMaxSpeed}, + InputSettingName{"MouseSmoothingMs", InputSetting::MouseSmoothingMs}, + InputSettingName{"MouseDecayMs", InputSetting::MouseDecayMs} }; const InputSettingName* findSetting(std::string_view name) { @@ -168,11 +180,33 @@ double parseMouseSensitivity(std::string_view value) { return parsed; } +std::string formatNumber(double value) { + std::array buffer{}; + const auto result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + return std::string(buffer.data(), result.ptr); +} + +double parseNumber(std::string_view value, std::string_view name, double minimum, double maximum) { + double parsed = 0.0; + const auto end = value.data() + value.size(); + const auto result = std::from_chars(value.data(), end, parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != end || !std::isfinite(parsed) || parsed < minimum || parsed > maximum) { + throw std::runtime_error(std::string(name) + " must be a number from " + formatNumber(minimum) + " to " + formatNumber(maximum)); + } + return parsed; +} + void applySetting(Pad::InputSettings& settings, InputSetting setting, std::string_view value) { switch (setting) { case InputSetting::LeftStickDeadzone: settings.leftStickDeadzonePercent = parseDeadzonePercent(value); break; case InputSetting::RightStickDeadzone: settings.rightStickDeadzonePercent = parseDeadzonePercent(value); break; case InputSetting::MouseSensitivity: settings.mouseSensitivity = parseMouseSensitivity(value); break; + case InputSetting::MouseVerticalScale: settings.mouseVerticalScale = parseNumber(value, "mouse vertical scale", 0.1, 10.0); break; + case InputSetting::MouseCurve: settings.mouseCurve = parseNumber(value, "mouse curve", 0.2, 5.0); break; + case InputSetting::MouseDeadzoneOffset: settings.mouseDeadzoneOffset = parseNumber(value, "mouse deadzone offset", 0.0, 0.9); break; + case InputSetting::MouseMaxSpeed: settings.mouseMaxSpeed = parseNumber(value, "mouse max speed", 0.1, 10.0); break; + case InputSetting::MouseSmoothingMs: settings.mouseSmoothingMs = parseNumber(value, "mouse smoothing", 0.0, 500.0); break; + case InputSetting::MouseDecayMs: settings.mouseDecayMs = parseNumber(value, "mouse decay", 1.0, 1000.0); break; } } diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp new file mode 100644 index 0000000000..d8cf0e3d7c --- /dev/null +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -0,0 +1,193 @@ +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#include +#include + +Pad::MouseStick::MouseStick(const InputSettings& settings) : settings(settings) {} + +void Pad::MouseStick::Move(int deltaX, int deltaY) { + pendingX += deltaX; + pendingY += deltaY; +} + +void Pad::MouseStick::Advance(double seconds) { + if (!(seconds > 0.0)) return; + pendingSeconds += seconds; + if (pendingSeconds < MouseMinimumStepSeconds) return; + const double step = pendingSeconds; + pendingSeconds = 0.0; + const bool moved = pendingX != 0.0 || pendingY != 0.0; + const double scale = settings.mouseSensitivity / (MouseFullTiltCountsPerSecond * step); + const double targetX = pendingX * scale; + const double targetY = pendingY * scale * settings.mouseVerticalScale; + pendingX = 0.0; + pendingY = 0.0; + idleSeconds = moved ? 0.0 : idleSeconds + step; + if (idleSeconds * 1000.0 >= settings.mouseDecayMs) { + speedX = 0.0; + speedY = 0.0; + return; + } + const double keep = settings.mouseSmoothingMs > 0.0 ? std::exp(-step * 1000.0 / settings.mouseSmoothingMs) : 0.0; + speedX = targetX + (speedX - targetX) * keep; + speedY = targetY + (speedY - targetY) * keep; + const double speed = std::hypot(speedX, speedY); + if (speed > settings.mouseMaxSpeed) { + speedX *= settings.mouseMaxSpeed / speed; + speedY *= settings.mouseMaxSpeed / speed; + } +} + +void Pad::MouseStick::Reset() { + pendingX = 0.0; + pendingY = 0.0; + pendingSeconds = 0.0; + speedX = 0.0; + speedY = 0.0; + idleSeconds = 0.0; +} + +double Pad::MouseStick::Speed() const { + return std::hypot(speedX, speedY); +} + +std::array Pad::MouseStick::Deflection() const { + const double speed = std::hypot(speedX, speedY); + if (speed <= 0.0) return {0.0, 0.0}; + const double shaped = std::pow(std::min(speed, 1.0), settings.mouseCurve); + const double deflection = settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * shaped; + return {speedX / speed * deflection, speedY / speed * deflection}; +} + +std::uint8_t Pad::StickByte(double deflection) { + const double clamped = std::clamp(deflection, -1.0, 1.0); + return static_cast(std::lround(clamped < 0.0 ? 128.0 + clamped * 128.0 : 128.0 + clamped * 127.0)); +} + +Pad::KeyboardMouse::KeyboardMouse(InputConfiguration configuration) + : configuration(std::move(configuration)), + pressed(this->configuration.bindings.size()), + wheelReleaseTimes(this->configuration.bindings.size()), + mouse(this->configuration.settings) {} + +const Pad::InputConfiguration& Pad::KeyboardMouse::Configuration() const { + return configuration; +} + +bool Pad::KeyboardMouse::MouseMode() const { + return mouseMode; +} + +bool Pad::KeyboardMouse::Advance(Clock::time_point now) { + if (!advanced) { + advanced = true; + lastAdvance = now; + } + if (now > lastAdvance) { + const double seconds = std::chrono::duration(now - lastAdvance).count(); + lastAdvance = now; + if (mouseMode) mouse.Advance(seconds); + } + bool released = false; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (configuration.bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; + pressed[index] = false; + wheelReleaseTimes[index] = {}; + released = true; + } + return released; +} + +void Pad::KeyboardMouse::SetMouseMode(bool enabled) { + mouseMode = enabled; + mouse.Reset(); +} + +void Pad::KeyboardMouse::Release() { + std::fill(pressed.begin(), pressed.end(), false); + std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), Clock::time_point{}); +} + +Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock::time_point now) { + InputRequests requests; + if (event.type == SDL_WINDOWEVENT) { + if (event.window.event != SDL_WINDOWEVENT_FOCUS_LOST && event.window.event != SDL_WINDOWEVENT_CLOSE) return requests; + Release(); + requests.releaseMouse = mouseMode; + return requests; + } + if (event.type == SDL_MOUSEMOTION) { + if (mouseMode) mouse.Move(event.motion.xrel, event.motion.yrel); + return requests; + } + if (event.type == SDL_MOUSEWHEEL) { + int direction = (event.wheel.y > 0) - (event.wheel.y < 0); + if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; + if (direction == 0) return requests; + const auto releaseTime = now + std::chrono::milliseconds(WheelPressDurationMs); + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + const auto& binding = configuration.bindings[index]; + if (binding.wheelDirection == 0) continue; + pressed[index] = binding.wheelDirection == direction; + wheelReleaseTimes[index] = pressed[index] ? releaseTime : Clock::time_point{}; + } + return requests; + } + const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; + const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!keyboard && !mouseButton) return requests; + if (keyboard && event.key.repeat != 0) return requests; + const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + const auto& binding = configuration.bindings[index]; + const bool matches = keyboard + ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode + : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); + if (!matches) continue; + if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; + pressed[index] = down; + } + return requests; +} + +PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const { + PadInputState state = controller; + std::array negative{}; + std::array positive{}; + for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { + if (!pressed[index]) continue; + const auto& binding = configuration.bindings[index]; + switch (binding.control) { + case InputControl::Button: + state.buttons |= static_cast(binding.button); + if (binding.button == PadButton::L2) state.analogButtonsL2 = 255; + if (binding.button == PadButton::R2) state.analogButtonsR2 = 255; + break; + case InputControl::LeftStickLeft: negative[0] = true; break; + case InputControl::LeftStickRight: positive[0] = true; break; + case InputControl::LeftStickUp: negative[1] = true; break; + case InputControl::LeftStickDown: positive[1] = true; break; + case InputControl::RightStickLeft: negative[2] = true; break; + case InputControl::RightStickRight: positive[2] = true; break; + case InputControl::RightStickUp: negative[3] = true; break; + case InputControl::RightStickDown: positive[3] = true; break; + case InputControl::TouchLeft: state.touchLeft = true; break; + case InputControl::TouchRight: state.touchRight = true; break; + case InputControl::ToggleMouse: break; + case InputControl::ToggleFullscreen: break; + } + } + for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { + if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; + } + if (mouseMode) { + const auto deflection = mouse.Deflection(); + state.sticks[2] = StickByte(deflection[0]); + state.sticks[3] = StickByte(deflection[1]); + } + if (state.analogButtonsL2 != 0) state.buttons |= static_cast(PadButton::L2); + if (state.analogButtonsR2 != 0) state.buttons |= static_cast(PadButton::R2); + return state; +} diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index f47aac9f3e..35c3228916 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -16,6 +16,14 @@ static constexpr std::uint32_t VIDEO_OUT_OUTPUT_OPTIONS_FIRST_WORD = 0x00FF0000; static constexpr std::size_t VIDEO_OUT_OUTPUT_OPTIONS_FREE_WORD = 3; +static VideoOutOutputStatus outputStatus(const auto& cfg) { + VideoOutOutputStatus status{}; + status.resolution = (cfg.width >= 3840 || cfg.height >= 2160) ? 2u : 1u; + status.dynamicRange = 1; + status.refreshRate = (cfg.outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; + return status; +} + int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { switch (mode) { case VIDEO_OUT_OUTPUT_MODE_DEFAULT: @@ -80,9 +88,7 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); } const int handle = VideoOutDriver::Get().Open(busType); - if (handle < 0) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY"); - } + if (handle < 0) return VIDEO_OUT_ERROR_RESOURCE_BUSY; return handle; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -161,13 +167,22 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu } std::unique_lock lock(cfg->mutex); cfg->Check(); - status->resolution = (cfg->width >= 3840 || cfg->height >= 2160) ? 2u : 1u; - status->dynamicRange = 1; - status->refreshRate = (cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ; - status->flags = 0; - status->reserved[0] = 0; - status->reserved[1] = 0; - status->reserved[2] = 0; + *status = outputStatus(*cfg); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) try { + if (status == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::unique_lock lock(cfg->mutex); + cfg->Check(); + std::array bytes{}; + const auto output = outputStatus(*cfg); + std::memcpy(bytes.data(), &output, sizeof(output)); + std::memcpy(status, bytes.data(), bytes.size()); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 5e5f7690a7..e5844a4b42 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -13,12 +13,12 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" PadInput::PadInput() : PadInput(Pad::LoadInputMapping()) { } -PadInput::PadInput(Pad::InputConfiguration configuration) - : bindings(std::move(configuration.bindings)), settings(configuration.settings), pressed(bindings.size()), wheelReleaseTimes(bindings.size()) { +PadInput::PadInput(Pad::InputConfiguration configuration) : keyboardMouse(std::move(configuration)) { openFirstAvailableController(); } @@ -112,13 +112,10 @@ void PadInput::applyOutput() { } void PadInput::setMouseMode(bool enabled) { + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); if (SDL_SetRelativeMouseMode(enabled ? SDL_TRUE : SDL_FALSE) != 0) throw std::runtime_error(std::string("Pad: relative mouse mode failed: ") + SDL_GetError()); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseEnabled = enabled; - mouseStick = {128, 128}; - nextMousePoll = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::MousePollIntervalMs); + keyboardMouse.SetMouseMode(enabled); } PadInputState PadInput::sampleController() const { @@ -160,8 +157,8 @@ PadInputState PadInput::sampleController() const { const auto stick = [this](SDL_GameControllerAxis xAxis, SDL_GameControllerAxis yAxis, int deadzonePercent) { return Pad::StickWithDeadzone(SDL_GameControllerGetAxis(controller, xAxis), SDL_GameControllerGetAxis(controller, yAxis), deadzonePercent); }; - const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, settings.leftStickDeadzonePercent); - const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, settings.rightStickDeadzonePercent); + const auto left = stick(SDL_CONTROLLER_AXIS_LEFTX, SDL_CONTROLLER_AXIS_LEFTY, keyboardMouse.Configuration().settings.leftStickDeadzonePercent); + const auto right = stick(SDL_CONTROLLER_AXIS_RIGHTX, SDL_CONTROLLER_AXIS_RIGHTY, keyboardMouse.Configuration().settings.rightStickDeadzonePercent); result.sticks = {left[0], left[1], right[0], right[1]}; switch (SDL_GameControllerGetType(controller)) { case SDL_CONTROLLER_TYPE_PS5: result.deviceKind = 1; break; @@ -213,137 +210,38 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { publish(); return; } - if (event.type == SDL_WINDOWEVENT && (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST || event.window.event == SDL_WINDOWEVENT_CLOSE)) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); - if (mouseEnabled) setMouseMode(false); + if (event.type == SDL_WINDOWEVENT) { + if (keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()).releaseMouse) setMouseMode(false); publish(); return; } // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach // its window (a stray press advances the title into another stage). static const bool ignoreInput = std::getenv("APS5_NO_PAD_INPUT") != nullptr; - if (ignoreInput && event.type == SDL_MOUSEWHEEL) return; - if (event.type == SDL_MOUSEWHEEL) { - int direction = (event.wheel.y > 0) - (event.wheel.y < 0); - if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; - if (direction == 0) return; - const auto releaseTime = std::chrono::steady_clock::now() + std::chrono::milliseconds(Pad::WheelPressDurationMs); - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - if (binding.wheelDirection == 0) continue; - pressed[index] = binding.wheelDirection == direction; - wheelReleaseTimes[index] = pressed[index] ? releaseTime : std::chrono::steady_clock::time_point{}; - } - publish(); - return; - } - const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; - const bool mouse = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; - if (!keyboard && !mouse) return; - if (ignoreInput) return; - if (keyboard && event.key.repeat != 0) return; - const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; - for (std::size_t index = 0; index < bindings.size(); ++index) { - const auto& binding = bindings[index]; - const bool matches = keyboard - ? binding.key != SDL_SCANCODE_UNKNOWN && binding.key == event.key.keysym.scancode - : binding.mouseButton != Pad::MouseButton::None && binding.mouseButton == static_cast(event.button.button); - - if (!matches) continue; - if (binding.control == Pad::InputControl::ToggleFullscreen) { - if (keyboard && down && !pressed[index] && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); - } - if (binding.control == Pad::InputControl::ToggleMouse && down && !pressed[index]) setMouseMode(!mouseEnabled); - pressed[index] = down; - } - publish(); + const bool input = event.type == SDL_MOUSEWHEEL || event.type == SDL_MOUSEMOTION || event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || + event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; + if (!input || ignoreInput) return; + if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; + 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.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); + if (event.type != SDL_MOUSEMOTION) publish(); } void PadInput::Update() { if (controller != nullptr) SDL_GameControllerUpdate(); applyOutput(); - if (controller != nullptr) { - controllerState = sampleController(); - publish(); - } - const auto now = std::chrono::steady_clock::now(); - bool released = false; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (bindings[index].wheelDirection == 0 || !pressed[index] || now < wheelReleaseTimes[index]) continue; - pressed[index] = false; - wheelReleaseTimes[index] = {}; - released = true; - } - if (released) publish(); - if (!mouseEnabled) return; - if (SDL_GetKeyboardFocus() == nullptr) { - std::fill(pressed.begin(), pressed.end(), false); - std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), std::chrono::steady_clock::time_point{}); + if (controller != nullptr) controllerState = sampleController(); + if (keyboardMouse.MouseMode() && SDL_GetKeyboardFocus() == nullptr) { + keyboardMouse.Release(); setMouseMode(false); publish(); return; } - if (now < nextMousePoll) return; - nextMousePoll = now + std::chrono::milliseconds(Pad::MousePollIntervalMs); - int deltaX = 0; - int deltaY = 0; - SDL_GetRelativeMouseState(&deltaX, &deltaY); - mouseStick = {128, 128}; - if (deltaX != 0 || deltaY != 0) { - const double distance = std::hypot(deltaX, deltaY); - const double scale = std::clamp(distance * settings.mouseSensitivity + 16.0, 64.0, 128.0) / distance; - const auto mapAxis = [scale](int delta) { return static_cast(std::clamp(128L + std::lround(delta * scale), 0L, 255L)); }; - mouseStick = {mapAxis(deltaX), mapAxis(deltaY)}; - } - publish(); + const bool changed = keyboardMouse.Advance(std::chrono::steady_clock::now()); + if (controller != nullptr || keyboardMouse.MouseMode() || changed) publish(); } void PadInput::publish() { - PadInputState state; - state.buttons = controllerState.buttons; - state.sticks = controllerState.sticks; - state.analogButtonsL2 = controllerState.analogButtonsL2; - state.analogButtonsR2 = controllerState.analogButtonsR2; - state.hasMotion = controllerState.hasMotion; - state.accel = controllerState.accel; - state.gyro = controllerState.gyro; - state.touch = controllerState.touch; - state.deviceKind = controllerState.deviceKind; - - std::array negative{}; - std::array positive{}; - for (std::size_t index = 0; index < bindings.size(); ++index) { - if (!pressed[index]) continue; - const auto& binding = bindings[index]; - switch (binding.control) { - case Pad::InputControl::Button: - state.buttons |= static_cast(binding.button); - if (binding.button == Pad::PadButton::L2) state.analogButtonsL2 = 255; - if (binding.button == Pad::PadButton::R2) state.analogButtonsR2 = 255; - break; - case Pad::InputControl::LeftStickLeft: negative[0] = true; break; - case Pad::InputControl::LeftStickRight: positive[0] = true; break; - case Pad::InputControl::LeftStickUp: negative[1] = true; break; - case Pad::InputControl::LeftStickDown: positive[1] = true; break; - case Pad::InputControl::RightStickLeft: negative[2] = true; break; - case Pad::InputControl::RightStickRight: positive[2] = true; break; - case Pad::InputControl::RightStickUp: negative[3] = true; break; - case Pad::InputControl::RightStickDown: positive[3] = true; break; - case Pad::InputControl::TouchLeft: state.touchLeft = true; break; - case Pad::InputControl::TouchRight: state.touchRight = true; break; - case Pad::InputControl::ToggleMouse: break; - case Pad::InputControl::ToggleFullscreen: break; - } - } - for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { - if (negative[axis] || positive[axis]) state.sticks[axis] = negative[axis] == positive[axis] ? 128 : negative[axis] ? 0 : 255; - } - if (mouseEnabled) { - state.sticks[2] = mouseStick[0]; - state.sticks[3] = mouseStick[1]; - } - if (state.analogButtonsL2 != 0) state.buttons |= static_cast(Pad::PadButton::L2); - if (state.analogButtonsR2 != 0) state.buttons |= static_cast(Pad::PadButton::R2); - PadPublishInput_nid_postfix(state); + PadPublishInput_nid_postfix(keyboardMouse.Compose(controllerState)); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 5b3dcdc23b..0decc71701 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -14,6 +14,7 @@ #include "prx/libSceVideoOut/include/MouseInput.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceVideoOut/include/KeyboardInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -259,6 +260,12 @@ VideoOutDriver& VideoOutDriver::Get() { VideoOutDriver::VideoOutDriver() { SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2"); +#ifdef _WIN32 + SDL_SetHintWithPriority(SDL_HINT_DIRECTINPUT_ENABLED, "0", SDL_HINT_DEFAULT); +#endif +#ifdef __linux__ + SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11"); +#endif if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError()); } @@ -337,7 +344,7 @@ int VideoOutDriver::Open(int busType) { uint64_t generation = 1; if (previous) { std::lock_guard cfgLock(previous->mutex); - require(!previous->opened, "port already open"); + if (previous->opened) return -1; require(previous->generation != std::numeric_limits::max(), "port generation overflow"); generation = previous->generation + 1; } @@ -594,6 +601,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star PadInput padInput; MouseInput mouseInput; KeyboardInput keyboardInput; + ImeTextInput imeTextInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); @@ -616,6 +624,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star if (window.Handle() != nullptr) { mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); + imeTextInput.HandleEvent(event, SDL_GetWindowID(window.Handle())); } } padInput.Update(); diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp index 2c5b74ad1d..a5105da79f 100644 --- a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -96,31 +96,52 @@ void checkDump(std::span dump, std::span decoded) { void testDumpQueue() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); - std::vector allocation(static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535); - const auto address = reinterpret_cast(allocation.data()); - const std::span storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u}; - const AgcDriver::DisplayBuffer display{reinterpret_cast(storage.data()), 0x8000000000000000ull, Width, Height}; - check(AgcDriver::DisplayBufferSize(display) == storage.size(), "incorrect padded display footprint"); - for (std::uint32_t y = 0; y < Height; ++y) { - for (std::uint32_t x = 0; x < Width; ++x) { - const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); - storage[offset] = static_cast(x & 255u); - storage[offset + 1] = static_cast(y & 255u); - storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); - storage[offset + 3] = std::byte{255}; + constexpr auto tiledBytes = static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536; + constexpr auto linearBytes = static_cast(Width) * Height * 4; + const auto makeStorage = [](std::size_t bytes, std::vector& allocation) { + allocation.resize(bytes + 65535); + const auto address = reinterpret_cast(allocation.data()); + auto* aligned = allocation.data() + ((65536u - (address & 65535u)) & 65535u); + return std::span{aligned, bytes}; + }; + std::vector tiledAllocation; + std::vector linearAllocation; + const auto tiledStorage = makeStorage(tiledBytes, tiledAllocation); + const auto linearStorage = makeStorage(linearBytes, linearAllocation); + const AgcDriver::DisplayBuffer tiled{reinterpret_cast(tiledStorage.data()), 0x8000000000000000ull, Width, Height}; + const AgcDriver::DisplayBuffer linear{reinterpret_cast(linearStorage.data()), 0x8000000000000000ull, Width, Height, 1}; + check(AgcDriver::DisplayBufferSize(tiled) == tiledStorage.size(), "incorrect padded tiled display footprint"); + check(AgcDriver::DisplayBufferSize(linear) == linearStorage.size(), "incorrect linear display footprint"); + const auto fill = [](const AgcDriver::DisplayBuffer& display, std::span storage) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = AgcDriver::DisplayBufferOffset(display, x, y); + storage[offset] = static_cast(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((x >> 8u) | ((y >> 8u) << 4u)); + storage[offset + 3] = std::byte{255}; + } } - } - const auto decoded = AgcDriver::ReadDisplayBuffer(display); - VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; - VideoOutBufferAttribute2 attribute{}; - sceVideoOutSetBufferAttribute2(&attribute, display.pixelFormat, 0, Width, Height, 0, 0, 0); - check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + }; + fill(tiled, tiledStorage); + fill(linear, linearStorage); + const auto tiledPixels = AgcDriver::ReadDisplayBuffer(tiled); + const auto linearPixels = AgcDriver::ReadDisplayBuffer(linear); + VideoOutBuffers tiledBuffer{tiledStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBuffers linearBuffer{linearStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 tiledAttribute{}; + VideoOutBufferAttribute2 linearAttribute{}; + sceVideoOutSetBufferAttribute2(&tiledAttribute, tiled.pixelFormat, 0, Width, Height, 0, 0, 0); + sceVideoOutSetBufferAttribute2(&linearAttribute, linear.pixelFormat, 1, Width, Height, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &tiledBuffer, 1, &tiledAttribute, 0, nullptr) == 0, "tiled buffer registration failed"); + check(sceVideoOutRegisterBuffers2(handle, 1, 1, &linearBuffer, 1, &linearAttribute, 0, nullptr) == 0, "linear buffer registration failed"); const HeldDump first(0); const HeldDump last(Frames - 1); std::exception_ptr failure; try { for (int frame = 0; frame < Frames; ++frame) { - check(sceVideoOutSubmitFlip(handle, 0, 1, frame) == 0, "the flip was not accepted"); + const int index = frame % 2; + check(sceVideoOutSubmitFlip(handle, index, 1, frame) == 0, "the flip was not accepted"); std::unique_lock lock(cfg->mutex); check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast(frame) + 1; }), "the flip did not complete"); if (cfg->failure) std::rethrow_exception(cfg->failure); @@ -130,11 +151,13 @@ void testDumpQueue() { } const auto firstDump = first.Release(); if (failure) std::rethrow_exception(failure); - checkDump(firstDump, decoded); + checkDump(firstDump, tiledPixels); const auto lastDump = last.Release(); - checkDump(lastDump, decoded); - for (int frame = 1; frame < Frames - 1; ++frame) checkDump(readDump(frame), decoded); + checkDump(lastDump, linearPixels); + checkDump(readDump(1), linearPixels); + checkDump(readDump(2), tiledPixels); sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutUnregisterBuffers(handle, 1); sceVideoOutClose(handle); LibcRunShutdown_nid_postfix(); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 0a67672ea6..daa2dad4d6 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -75,6 +75,36 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void mouseLookDefault() { + useConfig("Cross = KEY:F\n"); + const auto settings = Pad::LoadInputMapping().settings; + require(settings.mouseVerticalScale == 1.0 && settings.mouseCurve == 1.0 && settings.mouseMaxSpeed == 1.0, "mouse look scales default to 1"); + require(settings.mouseDeadzoneOffset == 0.2 && settings.mouseSmoothingMs == 10.0 && settings.mouseDecayMs == 30.0, "mouse look timing and offset defaults"); +} + +void mouseLookParse() { + useConfig("MouseCurve=0.7 ; softer start\nmousedeadzoneoffset = 0.15\nMouseMaxSpeed = 1.5\nMouseSmoothingMs = 0\nMouseDecayMs = 50\nMouseVerticalScale = 0.5\n"); + const auto settings = Pad::LoadInputMapping().settings; + require(settings.mouseCurve == 0.7, "mouse curve is read"); + require(settings.mouseDeadzoneOffset == 0.15, "mouse deadzone offset is read case-insensitively"); + require(settings.mouseMaxSpeed == 1.5, "mouse max speed is read"); + require(settings.mouseSmoothingMs == 0.0, "a 0 smoothing is accepted"); + require(settings.mouseDecayMs == 50.0, "mouse decay is read"); + require(settings.mouseVerticalScale == 0.5, "mouse vertical scale is read"); +} + +void mouseLookReject() { + require(rejects("MouseCurve = 0.1\n"), "a mouse curve below 0.2 is rejected"); + require(rejects("MouseCurve = steep\n"), "a non-numeric mouse curve is rejected"); + require(rejects("MouseDeadzoneOffset = 0.95\n"), "a deadzone offset above 0.9 is rejected"); + require(rejects("MouseDecayMs =\n"), "an empty decay is rejected"); + require(rejects("MouseDecayMs = 0\n"), "a zero decay is rejected"); + require(rejects("MouseSmoothingMs = 501\n"), "a smoothing above 500 is rejected"); + require(rejects("MouseMaxSpeed = nan\n"), "a non-finite max speed is rejected"); + require(rejects("MouseVerticalScale = 11\n"), "a vertical scale above 10 is rejected"); + require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -108,6 +138,9 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + mouseLookDefault(); + mouseLookParse(); + mouseLookReject(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp new file mode 100644 index 0000000000..93715508cb --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -0,0 +1,318 @@ +#include "prx/libScePad/include/InputMapping.hpp" +#include "prx/libScePad/include/KeyboardMouse.hpp" +#include "PadSimulation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using PadSimulation::At; +using PadSimulation::KeyEvent; +using PadSimulation::Sample; +using PadSimulation::Simulate; +using PadSimulation::TimedEvent; + +constexpr double Polls[] = {33.0, 8.0, 1.0}; + +void check(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +Pad::InputConfiguration defaults() { + return Pad::InputConfiguration{{Pad::InputMapping.begin(), Pad::InputMapping.end()}, {}}; +} + +Pad::InputConfiguration withSettings(const std::function& change) { + auto configuration = defaults(); + change(configuration.settings); + return configuration; +} + +double axis(std::uint8_t value) { + return value < 128 ? (static_cast(value) - 128.0) / 128.0 : (static_cast(value) - 128.0) / 127.0; +} + +double rightMagnitude(const Sample& sample) { + return std::hypot(axis(sample.state.sticks[2]), axis(sample.state.sticks[3])); +} + +double meanRightX(const std::vector& samples, double fromMs, double toMs) { + double sum = 0.0; + int count = 0; + for (const auto& sample : samples) { + if (sample.ms <= fromMs || sample.ms > toMs) continue; + sum += axis(sample.state.sticks[2]); + ++count; + } + check(count > 0, "no samples in window"); + return sum / count; +} + +double heldMeanRightX(const std::vector& samples, double pollMs, double fromMs, double toMs) { + double sum = 0.0; + for (const auto& sample : samples) { + if (sample.ms >= fromMs - 1e-9 && sample.ms < toMs - 1e-9) sum += axis(sample.state.sticks[2]) * pollMs; + } + return sum / (toMs - fromMs); +} + +std::vector mouseOnThenMotion(double durationMs, double countsPerSecondX, double countsPerSecondY = 0.0) { + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + PadSimulation::AppendMotion(events, 0.0, durationMs, countsPerSecondX, countsPerSecondY); + return events; +} + +double expectedDeflection(const Pad::InputSettings& settings, double normalizedSpeed) { + const double clamped = std::min(normalizedSpeed, settings.mouseMaxSpeed); + return settings.mouseDeadzoneOffset + (1.0 - settings.mouseDeadzoneOffset) * std::pow(std::min(clamped, 1.0), settings.mouseCurve); +} + +void testCenteredWithoutMotion() { + for (const double poll : Polls) { + const auto samples = Simulate(defaults(), {PadSimulation::MouseModeAt(0.0, true)}, poll, 300.0); + for (const auto& sample : samples) { + check(sample.state.sticks[2] == 128 && sample.state.sticks[3] == 128, "a resting mouse leaves the stick centered"); + } + } +} + +void testSteadyDeflectionFollowsCurve(const Pad::InputConfiguration& configuration, const char* name) { + const auto& settings = configuration.settings; + for (const double speed : {0.05, 0.25, 0.5, 0.75, 0.95, 3.0}) { + const double countsPerSecond = speed * Pad::MouseFullTiltCountsPerSecond / settings.mouseSensitivity; + const double expected = expectedDeflection(settings, speed); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(600.0, countsPerSecond), poll, 600.0); + const double measured = meanRightX(samples, 300.0, 600.0); + if (std::abs(measured - expected) > 0.02) { + throw std::runtime_error(std::string(name) + ": speed " + std::to_string(speed) + " at " + std::to_string(poll) + " ms polls gives " + + std::to_string(measured) + ", expected " + std::to_string(expected)); + } + } + } +} + +void testDeflectionCurve() { + testSteadyDeflectionFollowsCurve(defaults(), "defaults"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 2.0; s.mouseDeadzoneOffset = 0.1; s.mouseSensitivity = 2.0; }), "quadratic"); + testSteadyDeflectionFollowsCurve(withSettings([](auto& s) { s.mouseCurve = 0.5; s.mouseDeadzoneOffset = 0.0; }), "square root"); +} + +void testDeadzoneOffset() { + const auto configuration = defaults(); + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(400.0, 40.0), poll, 400.0); + bool moved = false; + for (const auto& sample : samples) { + const double magnitude = rightMagnitude(sample); + if (magnitude == 0.0) continue; + moved = true; + check(magnitude >= configuration.settings.mouseDeadzoneOffset - 0.01, "the slowest movement still clears the deadzone offset at " + std::to_string(poll) + " ms polls"); + } + check(moved, "slow movement moves the stick"); + } + const auto samples = Simulate(withSettings([](auto& s) { s.mouseDeadzoneOffset = 0.0; }), mouseOnThenMotion(400.0, 40.0), 8.0, 400.0); + check(meanRightX(samples, 200.0, 400.0) < 0.05, "without an offset slow movement stays small"); +} + +double centeredAfter(const std::vector& samples, double stopMs) { + double lastMoving = stopMs; + double previous = 2.0; + for (const auto& sample : samples) { + if (sample.ms <= stopMs) continue; + const double magnitude = rightMagnitude(sample); + check(magnitude <= previous + 1e-9, "the stick never grows after the mouse stops"); + previous = magnitude; + if (magnitude != 0.0) lastMoving = sample.ms; + } + return lastMoving - stopMs; +} + +void testDecay() { + const auto configuration = defaults(); + for (const double speed : {0.3, 1.0}) { + for (const double poll : Polls) { + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, speed * Pad::MouseFullTiltCountsPerSecond), poll, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail < configuration.settings.mouseDecayMs + poll, "stick centered within decay plus one poll at " + std::to_string(poll) + " ms polls, took " + std::to_string(tail)); + check(samples.back().state.sticks[2] == 128 && samples.back().state.sticks[3] == 128, "stick returns exactly to center"); + } + } + const auto slow = withSettings([](auto& s) { s.mouseDecayMs = 120.0; }); + const auto samples = Simulate(slow, mouseOnThenMotion(300.0, Pad::MouseFullTiltCountsPerSecond), 1.0, 600.0); + const double tail = centeredAfter(samples, 300.0); + check(tail >= 100.0 && tail < 121.0, "a longer decay keeps the stick moving longer"); +} + +void testMaxSpeedClamp() { + const auto configuration = defaults(); + constexpr double StopMs = 264.0; + for (const double poll : Polls) { + const auto flick = Simulate(configuration, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + const auto full = Simulate(configuration, mouseOnThenMotion(StopMs, 1.5 * Pad::MouseFullTiltCountsPerSecond), poll, 500.0); + check(flick[flick.size() / 3].state.sticks[2] == 255, "a fast flick reaches full tilt"); + check(centeredAfter(flick, StopMs) <= centeredAfter(full, StopMs) + 1e-9, "a flick faster than full tilt does not extend the tail"); + double flickSum = 0.0; + double fullSum = 0.0; + for (std::size_t index = 0; index < flick.size(); ++index) { + if (flick[index].ms <= StopMs) continue; + flickSum += rightMagnitude(flick[index]); + fullSum += rightMagnitude(full[index]); + } + check(std::abs(flickSum - fullSum) <= 0.02 * std::max(1.0, fullSum), "the stick decays from a flick like from full tilt at " + std::to_string(poll) + " ms polls"); + } + const auto headroom = withSettings([](auto& s) { s.mouseMaxSpeed = 3.0; }); + const auto flick = Simulate(headroom, mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + const auto full = Simulate(defaults(), mouseOnThenMotion(StopMs, 10.0 * Pad::MouseFullTiltCountsPerSecond), 1.0, 500.0); + check(rightMagnitude(flick[275]) > rightMagnitude(full[275]), "a higher max speed lets a flick hold full tilt longer"); +} + +void testSmoothing() { + const auto instant = Simulate(withSettings([](auto& s) { s.mouseSmoothingMs = 0.0; }), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 8.0, 100.0); + check(std::abs(axis(instant[1].state.sticks[2]) - expectedDeflection(defaults().settings, 0.5)) < 0.02, "without smoothing the stick follows at once"); + const auto smoothed = Simulate(defaults(), mouseOnThenMotion(100.0, Pad::MouseFullTiltCountsPerSecond / 2.0), 1.0, 100.0); + const double early = (axis(smoothed[9].state.sticks[2]) - 0.2) / 0.8; + check(early > 0.5 * 0.55 && early < 0.5 * 0.72, "smoothing reaches about 63 percent after one time constant"); +} + +std::vector waveMotion(double base, double swing) { + constexpr double Pi = 3.14159265358979323846; + std::vector events{PadSimulation::MouseModeAt(0.0, true)}; + for (int segment = 0; segment < 200; ++segment) { + const double speed = (base + swing * std::sin(2.0 * Pi * segment * 8.0 / 800.0)) * Pad::MouseFullTiltCountsPerSecond; + PadSimulation::AppendMotion(events, segment * 8.0, 8.0, speed, 0.3 * speed); + } + PadSimulation::AppendMotion(events, 1700.0, 200.0, 250.0, -120.0); + return events; +} + +double integratedRightMagnitude(const std::vector& samples, double pollMs) { + double sum = 0.0; + for (const auto& sample : samples) sum += rightMagnitude(sample) * pollMs / 1000.0; + return sum; +} + +void testPollRateIndependence() { + for (const auto& [name, events] : {std::pair{"one-way wave", waveMotion(0.5, 0.3)}, std::pair{"back-and-forth wave", waveMotion(0.0, 0.6)}}) { + std::vector> runs; + for (const double poll : Polls) runs.push_back(Simulate(defaults(), events, poll, 2112.0)); + const double reference = integratedRightMagnitude(runs[2], Polls[2]); + for (std::size_t run = 0; run < runs.size(); ++run) { + const double integrated = integratedRightMagnitude(runs[run], Polls[run]); + check(std::abs(integrated - reference) <= 0.02 * reference, std::string(name) + ": total deflection at " + std::to_string(Polls[run]) + " ms polls is " + + std::to_string(integrated) + ", 1 ms gives " + std::to_string(reference)); + const double tolerance = 0.03 + 0.0025 * Polls[run]; + for (double window = 0.0; window + 264.0 <= 2112.0; window += 264.0) { + const double mean = heldMeanRightX(runs[run], Polls[run], window, window + 264.0); + const double expected = heldMeanRightX(runs[2], Polls[2], window, window + 264.0); + check(std::abs(mean - expected) < tolerance, std::string(name) + ": held deflection at " + std::to_string(Polls[run]) + " ms polls from " + + std::to_string(window) + " ms is " + std::to_string(mean) + ", 1 ms gives " + std::to_string(expected)); + } + } + } +} + +void testVerticalScale() { + const auto configuration = withSettings([](auto& s) { s.mouseVerticalScale = 0.5; s.mouseDeadzoneOffset = 0.0; }); + const auto samples = Simulate(configuration, mouseOnThenMotion(300.0, 0.0, 0.8 * Pad::MouseFullTiltCountsPerSecond), 8.0, 300.0); + const double y = axis(samples.back().state.sticks[3]); + check(std::abs(y - 0.4) < 0.02, "vertical scale halves the vertical deflection"); +} + +void testMouseModeOff() { + std::vector events; + PadSimulation::AppendMotion(events, 0.0, 300.0, Pad::MouseFullTiltCountsPerSecond, 0.0); + for (const auto& sample : Simulate(defaults(), events, 8.0, 300.0)) { + check(sample.state.sticks[2] == 128, "motion without mouse mode leaves the stick alone"); + } + events.insert(events.begin(), At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true))); + events.insert(events.begin() + 1, At(0.0, PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false))); + const auto samples = Simulate(defaults(), events, 8.0, 300.0); + check(samples.back().state.sticks[2] == 255, "middle click turns mouse mode on"); +} + +void testKeyboardSticksUnchanged() { + const std::vector events{ + At(0.0, KeyEvent(SDL_SCANCODE_W, true)), + At(10.0, KeyEvent(SDL_SCANCODE_D, true)), + At(20.0, KeyEvent(SDL_SCANCODE_A, true)), + At(30.0, KeyEvent(SDL_SCANCODE_W, false)), + At(30.0, KeyEvent(SDL_SCANCODE_D, false)), + At(40.0, KeyEvent(SDL_SCANCODE_A, false))}; + const auto samples = Simulate(defaults(), events, 1.0, 50.0); + check(samples[4].state.sticks[1] == 0 && samples[4].state.sticks[0] == 128, "W tilts the left stick fully up"); + check(samples[14].state.sticks[0] == 255, "D tilts the left stick fully right"); + check(samples[24].state.sticks[0] == 128, "A and D together cancel"); + check(samples[34].state.sticks[0] == 0 && samples[34].state.sticks[1] == 128, "A alone tilts fully left"); + check(samples[44].state.sticks[0] == 128, "released keys center the stick"); +} + +void testWheelPress() { + const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1))}, 1.0, 200.0); + const auto up = static_cast(Pad::PadButton::Up); + check((samples[40].state.buttons & up) != 0, "wheel up presses Up"); + check((samples[120].state.buttons & up) == 0, "wheel press releases after its duration"); +} + + +void testWheelFocusLoss() { + const auto samples = Simulate(defaults(), {At(0.0, PadSimulation::WheelEvent(1)), At(10.0, KeyEvent(SDL_SCANCODE_W, true)), At(20.0, PadSimulation::FocusLostEvent())}, 1.0, 40.0); + check(samples[15].state.sticks[1] == 0, "a held key tilts the stick"); + check(samples[30].state.sticks[1] == 128, "losing focus lets go of held keys"); + check((samples[30].state.buttons & static_cast(Pad::PadButton::Up)) == 0, "losing focus lets go of the wheel press"); +} + +void testFocusLossReleasesMouse() { + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.SetMouseMode(true); + const auto requests = keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()); + check(requests.releaseMouse, "losing focus asks to release the captured mouse"); + keyboardMouse.SetMouseMode(false); + check(!keyboardMouse.HandleEvent(PadSimulation::FocusLostEvent(), Pad::KeyboardMouse::Clock::now()).releaseMouse, "nothing to release without capture"); +} + +void testToggleRequests() { + Pad::KeyboardMouse keyboardMouse(defaults()); + const auto now = Pad::KeyboardMouse::Clock::now(); + check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse"); + check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing"); + check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); +} + +} + +int main() { + try { + const std::pair> tests[] = { + {"centered without motion", testCenteredWithoutMotion}, + {"deflection curve", testDeflectionCurve}, + {"deadzone offset", testDeadzoneOffset}, + {"decay", testDecay}, + {"max speed clamp", testMaxSpeedClamp}, + {"smoothing", testSmoothing}, + {"poll rate independence", testPollRateIndependence}, + {"vertical scale", testVerticalScale}, + {"mouse mode off", testMouseModeOff}, + {"keyboard sticks unchanged", testKeyboardSticksUnchanged}, + {"wheel press", testWheelPress}, + {"wheel and keys on focus loss", testWheelFocusLoss}, + {"focus loss releases the mouse", testFocusLossReleasesMouse}, + {"toggle requests", testToggleRequests}, + }; + for (const auto& [name, test] : tests) { + test(); + std::printf("pad_keyboard_mouse: %s passed\n", name); + } + } catch (const std::exception& error) { + std::fprintf(stderr, "pad_keyboard_mouse: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp new file mode 100644 index 0000000000..75e0168901 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/PadSimulation.hpp @@ -0,0 +1,129 @@ +#ifndef CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP +#define CORE_LIBS_PRX_LIBSCEVIDEOOUT_TESTS_PADSIMULATION_HPP + +#include "prx/libScePad/include/KeyboardMouse.hpp" + +#include +#include +#include + +#include "SDL_keyboard.h" + +namespace PadSimulation { + +struct TimedEvent { + double ms = 0.0; + SDL_Event event{}; + bool setMouseMode = false; + bool mouseMode = false; + bool release = false; +}; + +struct Sample { + double ms = 0.0; + PadInputState state; +}; + +inline SDL_Event MotionEvent(int deltaX, int deltaY) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.xrel = deltaX; + event.motion.yrel = deltaY; + return event; +} + +inline SDL_Event KeyEvent(SDL_Scancode key, bool down) { + SDL_Event event{}; + event.type = down ? SDL_KEYDOWN : SDL_KEYUP; + event.key.keysym.scancode = key; + event.key.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event ButtonEvent(Uint8 button, bool down) { + SDL_Event event{}; + event.type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; + event.button.button = button; + event.button.state = down ? SDL_PRESSED : SDL_RELEASED; + return event; +} + +inline SDL_Event FocusLostEvent() { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.event = SDL_WINDOWEVENT_FOCUS_LOST; + return event; +} + +inline SDL_Event WheelEvent(int direction) { + SDL_Event event{}; + event.type = SDL_MOUSEWHEEL; + event.wheel.y = direction; + event.wheel.direction = SDL_MOUSEWHEEL_NORMAL; + return event; +} + +inline TimedEvent At(double ms, const SDL_Event& event) { + return {ms, event, false, false, false}; +} + +inline TimedEvent MouseModeAt(double ms, bool enabled) { + return {ms, SDL_Event{}, true, enabled, false}; +} + +inline TimedEvent ReleaseAt(double ms) { + return {ms, SDL_Event{}, false, false, true}; +} + +inline void AppendMotion(std::vector& events, double startMs, double durationMs, double countsPerSecondX, double countsPerSecondY, double reportHz = 1000.0) { + const double periodMs = 1000.0 / reportHz; + double owedX = 0.0; + double owedY = 0.0; + for (double ms = startMs + periodMs; ms <= startMs + durationMs + 1e-9; ms += periodMs) { + owedX += countsPerSecondX * periodMs / 1000.0; + owedY += countsPerSecondY * periodMs / 1000.0; + const int deltaX = static_cast(std::trunc(owedX)); + const int deltaY = static_cast(std::trunc(owedY)); + owedX -= deltaX; + owedY -= deltaY; + if (deltaX != 0 || deltaY != 0) events.push_back(At(ms, MotionEvent(deltaX, deltaY))); + } +} + +inline std::vector Simulate(const Pad::InputConfiguration& configuration, const std::vector& events, double pollMs, double endMs) { + using Clock = Pad::KeyboardMouse::Clock; + const auto origin = Clock::time_point{} + std::chrono::hours(1); + const auto at = [origin](double ms) { + return origin + std::chrono::duration_cast(std::chrono::duration(ms)); + }; + Pad::KeyboardMouse keyboardMouse(configuration); + keyboardMouse.Advance(at(0.0)); + std::vector samples; + std::size_t next = 0; + const auto steps = static_cast(std::floor(endMs / pollMs + 1e-9)); + for (long long step = 1; step <= steps; ++step) { + const double ms = static_cast(step) * pollMs; + const auto now = at(ms); + for (; next < events.size() && events[next].ms <= ms + 1e-9; ++next) { + const auto& timed = events[next]; + if (timed.release) { + keyboardMouse.Release(); + continue; + } + if (timed.setMouseMode) { + keyboardMouse.SetMouseMode(timed.mouseMode); + continue; + } + const auto requests = keyboardMouse.HandleEvent(timed.event, now); + if (requests.toggleMouse) keyboardMouse.SetMouseMode(!keyboardMouse.MouseMode()); + if (requests.releaseMouse) keyboardMouse.SetMouseMode(false); + } + keyboardMouse.Advance(now); + samples.push_back({ms, keyboardMouse.Compose(PadInputState{})}); + } + return samples; +} + +} + +#endif diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 8f2342a25b..62f3b7a43b 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -48,6 +48,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/RealPath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Threads.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) @@ -214,7 +215,7 @@ if(BUILD_TESTING) 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) + target_link_libraries(guest_libc_threads_tests PRIVATE libc libkernel) configure_windows_unwind(guest_libc_threads_tests) add_test(NAME guest_libc_threads COMMAND guest_libc_threads_tests) set_tests_properties(guest_libc_threads PROPERTIES TIMEOUT 20) @@ -584,6 +585,16 @@ if(BUILD_TESTING) set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_allocator_failures_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatorFailures.cpp) + target_include_directories(guest_allocator_failures_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocator_failures_tests PRIVATE libc) + configure_windows_unwind(guest_allocator_failures_tests) + add_test(NAME guest_allocator_failures COMMAND guest_allocator_failures_tests) + set_tests_properties(guest_allocator_failures PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_allocator_failures PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(application_heap_stats_tests ${LIBS_INCLUDE_DIR}/tests/ApplicationHeapStats.cpp) target_include_directories(application_heap_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_stats_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 231826bb92..ce42a0be61 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -83,6 +83,7 @@ void GuestAllocationsInvalidate_nid_postfix(std::uintptr_t address, std::size_t // until the lease is dropped by another thread. void GuestAllocationsSetPinWaiter_nid_postfix(bool (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsSetGpuMapObserver_nid_postfix(void (*callback)(const Mapped& ranges, std::uint64_t generation)); +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes)); void GuestAllocationsNoteGpuMapping_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 5029a7717a..3c96845e4d 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -30,6 +30,7 @@ void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); #ifdef _WIN32 bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address); +bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address); void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes); bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection); @@ -40,8 +41,11 @@ bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_ std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void* 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); #else void GuestArenaSetSharedBacking_nid_postfix(SharedBackingResolver resolver); void GuestArenaSetSharedBackingWriter_nid_no_patch(SharedBackingWriter writer); diff --git a/core/libs/prx/libc/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp new file mode 100644 index 0000000000..529011b509 --- /dev/null +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -0,0 +1,36 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSDESCRIPTOR_HPP + +#include +#include +#include +#include +#include +#include + +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); + +static inline HANDLE WindowsDescriptorHandle_nid_no_patch(int descriptor) { + const auto ignore = +[](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}; + const auto previous = _set_thread_local_invalid_parameter_handler(ignore); + const auto handle = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +static inline int WindowsDescriptorAccess_nid_no_patch(HANDLE handle, int descriptor, const char* function) { + using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); + PUBLIC_OBJECT_BASIC_INFORMATION information{}; + const NTSTATUS status = query == nullptr ? -1 : query(handle, ObjectBasicInformation, &information, sizeof(information), nullptr); + if (status < 0) + throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(descriptor) + ", status=" + std::to_string(status)); + const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; + const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + if (read && write) return 2; + if (write) return 1; + if (read) return 0; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(descriptor) + " grants neither read nor write access"); +} + +#endif diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index d93c344e62..8df63459a7 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ public: std::vector> Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { std::vector> created; std::lock_guard lock(mutex); + collectableRanges.clear(); const auto end = reinterpret_cast(address) + bytes; for (auto cursor = reinterpret_cast(address); cursor < end;) { const auto memory = query(cursor); @@ -74,38 +76,78 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) { std::lock_guard lock(mutex); + collectableRanges.clear(); auto cursor = reinterpret_cast(address); reset(cursor, bytes); + if (resident) watchResident = watched; HANDLE duplicate = nullptr; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); - for (std::size_t done = 0; done < bytes; done += pageBytes) { - split(cursor + done, pageBytes); - void* page = reinterpret_cast(cursor + done); - if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page"); - if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); - DWORD previous; - if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); - const auto key = std::make_pair(reinterpret_cast(section), offset + done); - auto shared = physical[key].lock(); + const auto slotOf = [&](std::size_t done) -> const std::shared_ptr& { + auto& slot = physical[std::make_pair(reinterpret_cast(section), offset + done)]; + if (!slot) slot = std::make_shared(); + return slot; + }; + const auto pageOf = [](Physical& slot) { + auto shared = slot.page.lock(); if (!shared) { shared = std::make_shared(); - physical[key] = shared; + slot.page = shared; } + return shared; + }; + const auto single = [&](std::size_t done) { + const auto& slot = *slotOf(done); + const auto shared = slot.page.lock(); + return resident && !slot.stored && (!shared || shared->aliases.empty()); + }; + for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; + if (single(done)) { + auto runBytes = pageBytes; + while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; + split(base, runBytes); + commitResident(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); + const auto shared = pageOf(*slot); + if (slot->saved) { + std::memcpy(reinterpret_cast(base + at), slot->saved->bytes + slot->savedOffset, pageBytes); + slot->saved.reset(); + } + shared->aliases.push_back(base + at); + views.emplace(base + at, View{shared, protection, {}, 0, owned, offset + done + at, 0, 0, true, slot}); + invalidate(*shared); + } + remember(freshRanges, base, base + runBytes); + DWORD previous; + if (protection != PAGE_READWRITE && !VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect resident guest pages"); + done += runBytes; + continue; + } + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + split(base, pageBytes); + mapShared(base, section, offset + done, protection); + slot->stored = true; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot}); remember(freshRanges, base, base + pageBytes); invalidate(*shared); + done += pageBytes; } } void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { std::lock_guard lock(mutex); + collectableRanges.clear(); for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; it->second.armed = 0; @@ -143,6 +185,10 @@ 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 memory = query(address); + return memory.State == MEM_COMMIT && writable(memory.Protect & 0xffu); + } const auto written = subpagesOf(base, address, address + 1); invalidate(*view.page, written); protectSubpages(base, written, view.protection, "resume shared memory write"); @@ -150,6 +196,29 @@ public: return true; } + bool HandleRead(std::uintptr_t address) { + std::lock_guard lock(mutex); + if (!views.contains(address & ~(pageBytes - 1))) return false; + const auto memory = query(address); + return memory.State == MEM_COMMIT && (memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; + } + + bool ResidentSpan(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(mutex); + auto run = residentRuns.upper_bound(address); + if (run != residentRuns.begin() && std::prev(run)->second.end > address) --run; + if (run == residentRuns.end() || run->first >= address + bytes) return false; + *start = run->first; + for (; run != residentRuns.end() && run->first < address + bytes; ++run) *end = run->second.end; + return true; + } + + void 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}))); + } + bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); @@ -187,6 +256,16 @@ public: return std::runtime_error(text); }; if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) throw refuse("the range is not made of whole shared pages"); + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto found = views.find(address + done); + if (found == views.end() || !found->second.resident) continue; + const auto run = std::prev(residentRuns.upper_bound(address + done)); + if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range"); + } + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto found = views.find(address + done); + if (found != views.end() && found->second.resident) demote(address + done); + } auto view = views.find(address); if (view == views.end()) throw refuse("the range does not start at a shared view"); const auto section = view->second.section; @@ -244,12 +323,17 @@ public: const auto fresh = freshRanges.upper_bound(address); if ((fresh == freshRanges.begin() || std::prev(fresh)->second <= address) && (fresh == freshRanges.end() || fresh->first >= end)) return true; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { + if (it->second.resident) { + if (!watchResident) return false; + continue; + } if (it->second.protection != PAGE_NOACCESS) continue; for (auto at = std::max(address, it->first); at < std::min(end, it->first + pageBytes); at += 4096) { if ((it->second.collected & (1u << ((at - it->first) / 4096))) == 0) return false; } } } else if (clear && !collectable(address, end)) return false; + auto position = views.lower_bound(address & ~(pageBytes - 1)); for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -260,8 +344,17 @@ public: } } const auto base = cursor & ~(pageBytes - 1); - const auto found = views.find(base); - if (found != views.end()) { + const auto found = viewAt(base, position); + if (found != views.end() && found->second.resident) { + if (!watchResident) return false; + 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}); + collectResident(run->first, run->second, cursor, stop, pages, count, clear, collection); + if (*count == capacity) return true; + cursor = stop; + } else if (found != views.end()) { auto& view = found->second; auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); if (collection != Collection::All) { @@ -309,6 +402,7 @@ public: std::uint8_t untracked = 0; for (const auto alias : view.page->aliases) { auto& other = views.at(alias); + if (other.resident) continue; const auto arming = static_cast(settled & ~other.armed); if (!writable(other.protection) || arming == 0) continue; if (other.hostWrites != 0) { @@ -346,8 +440,9 @@ public: const auto memory = query(cursor); if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); - ULONG_PTR available = capacity - *count; - if (available == 0) return true; + if (capacity == *count) return true; + const auto touched = (((stop + 4095) & ~static_cast(4095)) - (cursor & ~static_cast(4095))) / 4096; + ULONG_PTR available = std::min(capacity - *count, touched); DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); *count += available; @@ -360,10 +455,13 @@ public: private: bool collectable(std::uintptr_t address, std::uintptr_t end) { + const auto proven = collectableRanges.upper_bound(address); + if (proven != collectableRanges.begin() && std::prev(proven)->second >= end) return true; + auto position = views.lower_bound(address & ~(pageBytes - 1)); for (auto cursor = address; cursor < end;) { const auto base = cursor & ~(pageBytes - 1); - if (const auto found = views.find(base); found != views.end()) { - if (found->second.protection == PAGE_NOACCESS) return false; + if (const auto found = viewAt(base, position); found != views.end()) { + if (found->second.protection == PAGE_NOACCESS || (found->second.resident && !watchResident)) return false; cursor = std::min(end, base + pageBytes); continue; } @@ -371,6 +469,7 @@ private: if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); } + remember(collectableRanges, address, end); return true; } @@ -379,6 +478,7 @@ private: static_assert(subpageBytes == 4096, "Collect reports one page per subpage"); static constexpr std::size_t subpages = pageBytes / subpageBytes; static constexpr std::uint8_t allSubpages = (1u << subpages) - 1; + static constexpr std::size_t chunkBytes = 0x200000; struct SharedPage { std::array generation = [] { std::array initial{}; @@ -395,6 +495,19 @@ private: Section& operator=(const Section&) = delete; ~Section() { CloseHandle(handle); } }; + struct Saved { + unsigned char* bytes; + explicit Saved(unsigned char* bytes) : bytes(bytes) {} + Saved(const Saved&) = delete; + Saved& operator=(const Saved&) = delete; + ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + }; + struct Physical { + std::weak_ptr page; + bool stored = false; + std::shared_ptr saved; + std::size_t savedOffset = 0; + }; struct View { std::shared_ptr page; DWORD protection; @@ -404,9 +517,159 @@ private: std::uint64_t offset; std::uint32_t hostWrites; std::uint8_t collected = 0; + bool resident = false; + std::shared_ptr slot; + }; + struct Run { + std::uintptr_t end; + std::array collected; + std::uint32_t fresh; + }; + struct Window { + void* view = nullptr; + unsigned char* bytes = nullptr; }; using Ranges = std::map; + static bool runCollected(const Run& run, std::size_t bit) { + return (run.collected[bit >> 6] >> (bit & 63) & 1) != 0; + } + + 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}; + std::vector written; + if (collection != Collection::Fresh) { + std::vector found((stop - first + 4095) / 4096); + ULONG_PTR reported = found.size(); + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(first), stop - first, found.data(), &reported, &granularity) != 0) fail("collect resident guest writes"); + for (ULONG_PTR index = 0; index < reported; ++index) written.push_back(reinterpret_cast(found[index])); + } + auto next = written.begin(); + std::uintptr_t resetFrom = 0; + const auto flushReset = [&](std::uintptr_t to) { + if (resetFrom != 0 && ResetWriteWatch(reinterpret_cast(resetFrom), to - resetFrom) != 0) fail("reset resident guest writes"); + resetFrom = 0; + }; + for (auto at = first; at < stop; at += 4096) { + const auto bit = (at - start) >> 12; + const bool fresh = !runCollected(run, bit); + bool wrote = false; + while (next != written.end() && *next < at) ++next; + if (next != written.end() && *next == at) wrote = true; + const bool report = collection == Collection::Fresh ? fresh : collection == Collection::Armed ? !fresh && wrote : fresh || wrote; + if (report) pages[(*count)++] = reinterpret_cast(std::max(at, from)); + const bool settle = clear && fresh && collection != Collection::Armed; + if (settle) { + run.collected[bit >> 6] |= 1ull << (bit & 63); + --run.fresh; + forget(freshRanges, at, at + 4096); + if (collection == Collection::Fresh && resetFrom == 0) resetFrom = at; + } else if (collection == Collection::Fresh) { + flushReset(at); + } + } + if (collection == Collection::Fresh) flushReset(stop); + } + + Window open(HANDLE section, std::uint64_t offset, std::size_t bytes) { + const auto lead = offset % granularity; + Window window; + window.view = map(section, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); + if (window.view == nullptr) fail("open a window on direct memory"); + window.bytes = static_cast(window.view) + lead; + return window; + } + + void close(Window& window) { + if (window.view != nullptr && !unmap(GetCurrentProcess(), window.view, 0)) fail("close a window on direct memory"); + window = {}; + } + + void commitResident(std::uintptr_t base, std::size_t bytes) { + const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watchResident ? MEM_WRITE_WATCH : 0); + if (!allocate(GetCurrentProcess(), reinterpret_cast(base), bytes, flags, PAGE_READWRITE, nullptr, 0)) fail("replace guest placeholder with resident direct memory"); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) { + void* page = reinterpret_cast(base); + if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + DWORD previous; + if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + } + + static bool blank(std::uintptr_t page) { + const auto* words = reinterpret_cast(page); + for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { + if (words[i] != 0) return false; + } + return true; + } + + void demote(std::uintptr_t address) { + const auto run = std::prev(residentRuns.upper_bound(address)); + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages"); + const auto section = views.at(start).section; + auto window = open(section->handle, views.at(start).offset, stop - start); + if (query(reinterpret_cast(window.bytes)).State == MEM_RESERVE && !VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); + std::memcpy(window.bytes, reinterpret_cast(start), stop - start); + close(window); + if (!VirtualFree(reinterpret_cast(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages"); + for (auto page = start; page < stop; page += pageBytes) { + auto& view = views.at(page); + split(page, pageBytes); + mapShared(page, view.section->handle, view.offset, view.protection); + view.slot->stored = true; + view.resident = false; + view.seen.fill(0); + view.armed = 0; + view.collected = 0; + for (std::size_t sub = 0; sub < subpages; ++sub) { + if (runCollected(run->second, ((page - start) >> 12) + sub)) view.collected |= static_cast(1u << sub); + } + invalidate(*view.page); + } + residentRuns.erase(run); + } + + void breakRun(std::uintptr_t at) { + const auto next = residentRuns.upper_bound(at); + if (next == residentRuns.begin()) return; + const auto run = std::prev(next); + if (at > run->first && at < run->second.end) demote(at); + } + + void evict(std::map::iterator run) { + const auto start = run->first; + const auto stop = run->second.end; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("open resident guest pages"); + std::shared_ptr saved; + for (auto page = start; page < stop; page += pageBytes) { + 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"); + std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); + view.slot->saved = saved; + view.slot->savedOffset = page - start; + } + std::erase(view.page->aliases, page); + views.erase(found); + } + forget(freshRanges, start, stop); + residentRuns.erase(run); + } + 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; @@ -489,6 +752,9 @@ private: unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); compare = reinterpret_cast(GetProcAddress(module, "CompareObjectHandles")); + SYSTEM_INFO system{}; + GetSystemInfo(&system); + granularity = system.dwAllocationGranularity; } [[noreturn]] static void fail(const char* operation) { @@ -515,8 +781,11 @@ private: } void reset(std::uintptr_t address, std::size_t bytes) { + collectableRanges.clear(); const auto end = address + bytes; forgetClean(address, end); + breakRun(address); + breakRun(end); for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -540,6 +809,7 @@ private: forget(freshRanges, cursor, cursor + pageBytes); } } else if (memory.Type == MEM_PRIVATE) { + if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); } else { throw std::runtime_error("unsupported guest mapping type"); @@ -591,10 +861,24 @@ private: } Ranges cleanRanges; + Ranges collectableRanges; Ranges freshRanges; std::map views; - std::map, std::weak_ptr> physical; + + using Views = std::map; + + Views::iterator viewAt(std::uintptr_t base, Views::iterator& position) { + if (position != views.end() && position->first < base) { + ++position; + if (position != views.end() && position->first < base) position = views.lower_bound(base); + } + return position != views.end() && position->first == base ? position : views.end(); + } + std::map, std::shared_ptr> physical; + std::map residentRuns; std::map splitAliases; + std::size_t granularity = 0x10000; + bool watchResident = false; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/prx/libc/include/general/AsmMacros.hpp b/core/libs/prx/libc/include/general/AsmMacros.hpp index 0962f0c917..78de31dbd6 100644 --- a/core/libs/prx/libc/include/general/AsmMacros.hpp +++ b/core/libs/prx/libc/include/general/AsmMacros.hpp @@ -1,10 +1,18 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_ASMMACROS_HPP -#ifdef _WIN32 +#if defined(_WIN32) +#define APS5_ASM_SYMBOL(name) name #define APS5_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" +#elif defined(__APPLE__) +#define APS5_ASM_SYMBOL(name) "_" name +#define APS5_ASM_FUNCTION(name) ".globl _" name "\n_" name ":\n" +#define APS5_ASM_FUNCTION_END(name) "" #else +#define APS5_ASM_SYMBOL(name) name #define APS5_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" +#define APS5_ASM_FUNCTION_END(name) ".size " name ", .-" name "\n" #endif #endif diff --git a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp index 53d45d895b..f4957fd7b3 100644 --- a/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp +++ b/core/libs/prx/libc/include/specifics/itanium/CxxAbi.hpp @@ -39,6 +39,8 @@ public: class __pointer_type_info : public __pbase_type_info {}; +class __function_type_info : public std::type_info {}; + class __pointer_to_member_type_info : public __pbase_type_info { public: const __class_type_info* __context; diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp new file mode 100644 index 0000000000..5286225707 --- /dev/null +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -0,0 +1,16 @@ +#include +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index ef219f06bb..69e7f8d0fe 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -19,6 +19,7 @@ #include #include #include +#include "prx/libc/include/WindowsDescriptor.hpp" #endif #include "prx/libc/include/FileStream.hpp" @@ -210,6 +211,25 @@ static void SortElements(unsigned char* base, size_t count, size_t size, GuestCo } } +namespace { + +template +void* allocationOrNull(TAllocate allocate) { + try { + return allocate(); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + errno = 12; + return nullptr; +} + +bool validAlignment(std::size_t alignment) { + return alignment != 0 && (alignment & (alignment - 1)) == 0; +} + +} + extern "C" { [[noreturn]] void APS5_VABI _ZSt11_Xbad_allocv_nid_postfix(); @@ -225,6 +245,11 @@ FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { const auto release = [](void* pointer) { ::operator delete(pointer); }; std::unique_ptr storage(::operator new(sizeof(FileStream)), release); #ifdef _WIN32 + const auto handle = WindowsDescriptorHandle_nid_no_patch(descriptor); + if (handle == INVALID_HANDLE_VALUE) { errno = 9; return nullptr; } + const int access = WindowsDescriptorAccess_nid_no_patch(handle, descriptor, "fdopen"); + const int requested = nativeMode.find('+') != std::string::npos ? 2 : nativeMode[0] == 'r' ? 0 : 1; + if (access != 2 && access != requested) { errno = 22; return nullptr; } if (parsed->append && !AppendThroughDescriptor(descriptor)) return nullptr; auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else @@ -274,29 +299,11 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) } std::filesystem::path fpath; if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } - const auto abs_path = fpath.string(); std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { const int error = errno; - if (error == ENOENT || (parsed->exclusive && error == EEXIST)) { - errno = error; - return nullptr; - } - const auto reason = std::strerror(error); - std::error_code ec; - std::filesystem::path sibling; - for (const auto& entry : std::filesystem::directory_iterator(fpath.parent_path(), ec)) { - if (ec) break; - if (entry.path().stem() == fpath.stem() && entry.path().extension() != fpath.extension()) { - sibling = std::filesystem::absolute(entry.path()); - break; - } - } - if (!sibling.empty()) { - // APS5_LOG_OUT("%s: \"%s\": %s. Sibling: \"%s\"", FOPEN_MSG_NOT_FOUND, abs_path.c_str(), reason, sibling.filename().string().c_str()); - return nullptr; - } - throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_OPEN_FAILED + ": \"" + abs_path + "\": " + reason); + errno = error; + return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); if (parsed->append) static_cast(std::fseek(handle.get(), 0, SEEK_END)); @@ -438,8 +445,9 @@ int APS5_VABI fflush_nid_postfix(FileStream* stream) { #ifdef __linux__ if (native && !__fwriting(native)) return 0; #endif - if (std::fflush(native) != 0) throw std::runtime_error("fflush: flush failed"); - return 0; + const int result = std::fflush(native); + if (stream) stream->SyncStatus(); + return result; } int APS5_VABI malloc_stats_fast_nid_postfix(void* stats) { @@ -451,7 +459,7 @@ size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { } void* APS5_VABI malloc_nid_postfix(size_t size) { - return ApplicationHeapAllocate_nid_no_patch(size); + return allocationOrNull([size] { return ApplicationHeapAllocate_nid_no_patch(size); }); } void APS5_VABI free_nid_postfix(void* ptr) { @@ -459,7 +467,7 @@ void APS5_VABI free_nid_postfix(void* ptr) { } void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { - return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + return allocationOrNull([ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { @@ -474,19 +482,27 @@ void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + if (!validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); } void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { - return ApplicationHeapAlign_nid_no_patch(alignment, size); + return memalign_nid_postfix(alignment, size); } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { - return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); + if (size != 0 && !validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { - return ApplicationHeapCalloc_nid_no_patch(count, size); + return allocationOrNull([count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }); } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { diff --git a/core/libs/prx/libc/src/FileLock.cpp b/core/libs/prx/libc/src/FileLock.cpp index 1ed2279816..40efecd962 100644 --- a/core/libs/prx/libc/src/FileLock.cpp +++ b/core/libs/prx/libc/src/FileLock.cpp @@ -15,6 +15,8 @@ constexpr int LockShared = 1; constexpr int LockExclusive = 2; constexpr int LockNonblocking = 4; constexpr int LockUnlock = 8; +constexpr DWORD LockByteLow = 0xFFFFFFFE; +constexpr DWORD LockByteHigh = 0x7FFFFFFF; struct LockState { std::mutex mutex; @@ -55,14 +57,18 @@ int GuestFileLock_nid_no_patch(int fd, int operation) { if (state->mode == mode) return 0; if (state->mode != 0) { OVERLAPPED overlapped{}; - if (!::UnlockFileEx(handle, 0, MAXDWORD, MAXDWORD, &overlapped)) return -1; + overlapped.Offset = LockByteLow; + overlapped.OffsetHigh = LockByteHigh; + if (!::UnlockFileEx(handle, 0, 1, 0, &overlapped)) return -1; state->mode = 0; } if (mode == LockUnlock) return 0; DWORD flags = mode == LockExclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0; if (operation & LockNonblocking) flags |= LOCKFILE_FAIL_IMMEDIATELY; OVERLAPPED overlapped{}; - if (!::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped)) return -1; + overlapped.Offset = LockByteLow; + overlapped.OffsetHigh = LockByteHigh; + if (!::LockFileEx(handle, flags, 0, 1, 0, &overlapped)) return -1; state->mode = mode; return 0; } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 1aa78ba067..e9a5661781 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -259,6 +259,63 @@ int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const return result; } +int APS5_VABI vswprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI swprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && size != 0 && size <= RsizeMax) { + APS5_VA_BEGIN(format); + try { + const std::u16string text = FormatWide(format, reinterpret_cast(args), true); + if (text.size() + 1 <= size && text.size() <= static_cast(INT_MAX)) { + std::memcpy(buffer, text.data(), text.size() * sizeof(char16_t)); + buffer[text.size()] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + APS5_VA_END(); + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + +int APS5_VABI vsnwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { + constexpr std::size_t RsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + int result = -1; + if (buffer != nullptr && format != nullptr && args != nullptr && size != 0 && size <= RsizeMax) { + try { + const std::u16string text = FormatWide(format, args, true); + if (text.size() <= static_cast(INT_MAX)) { + const std::size_t copied = text.size() < size - 1 ? text.size() : size - 1; + std::memcpy(buffer, text.data(), copied * sizeof(char16_t)); + buffer[copied] = 0; + result = static_cast(text.size()); + } + } catch (const std::invalid_argument&) { + } + } + if (result < 0 && buffer != nullptr && size != 0 && size <= RsizeMax) buffer[0] = 0; + return result; +} + int APS5_VABI wprintf_nid_postfix(const char16_t* format, ...) { APS5_VA_BEGIN(format); int result = -1; diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 133196f578..402da585ff 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -107,6 +107,9 @@ std::optional ResolveAlias(const std::string& guestPath) if (!HasPrefix(relative, prefix)) continue; if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred(); std::filesystem::path result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); +#ifndef _WIN32 + if (guestPath.back() == '/') result /= ""; +#endif return result.make_preferred(); } return std::nullopt; @@ -384,7 +387,16 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file auto relinked = path; relinked += ".guest.prx"; std::error_code error; - return std::filesystem::is_regular_file(relinked, error) ? relinked : path; + if (std::filesystem::is_regular_file(relinked, error)) return relinked; + const auto name = relinked.filename().string(); + std::optional matched; + std::filesystem::directory_iterator entry(relinked.parent_path().empty() ? "." : relinked.parent_path(), error), end; + for (; !error && entry != end; entry.increment(error)) { + if (!SameName(entry->path().filename().string(), name)) continue; + if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); + matched = entry->path(); + } + return matched && std::filesystem::is_regular_file(*matched, error) ? *matched : path; } extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 1faac83136..c7b286af87 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -49,6 +49,7 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; std::atomic gpuMapObserver{nullptr}; +std::atomic unmapObserver{nullptr}; std::atomic gpuMapObserverCalls{0}; std::chrono::milliseconds pinWait() { @@ -421,9 +422,10 @@ void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer) { const auto exact = ranges.find(range.address); if (exact != ranges.end() && exact->second->allocationBytes == exact->second->bytes) { ranges.erase(exact); - return; + } else { + std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); } - std::erase_if(ranges, [&](const auto& entry) { return entry.second->allocationAddress == range.address; }); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(range.address, range.bytes); } namespace { @@ -517,10 +519,16 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const bool last = !allocationRemains(range, patch); apply(reinterpret_cast(cursor), pieceEnd - cursor, reinterpret_cast(range.allocationAddress), last); applyPatch(patch); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(cursor, pieceEnd - cursor); any = true; cursor = pieceEnd; } require(any, "unmap address is not registered"); + if (const auto observer = unmapObserver.load(std::memory_order_acquire)) observer(reinterpret_cast(pointer), bytes); +} + +void GuestAllocationsSetUnmapObserver_nid_postfix(void (*callback)(std::uintptr_t, std::size_t)) { + unmapObserver.store(callback, std::memory_order_release); } Lease GuestAllocationsAcquire_nid_postfix() { diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5ff89ebc9c..5387ae0390 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -320,6 +320,10 @@ bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) { return WindowsMappings::Get().HandleWrite(address); } +bool GuestArenaHandleRead_nid_postfix(std::uintptr_t address) { + return WindowsMappings::Get().HandleRead(address); +} + void GuestArenaPinWritable_nid_postfix(const void* pointer, std::size_t bytes) { if (!Arena::Get().Contains(pointer, bytes)) return; WindowsMappings::Get().Pin(reinterpret_cast(pointer), bytes); @@ -374,7 +378,12 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_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("shared mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false); +} + +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); } void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) { @@ -384,6 +393,14 @@ void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) void GuestArenaUnmapAlias_nid_postfix(void* alias) { WindowsMappings::Get().UnmapAlias(alias); } + +bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end) { + return WindowsMappings::Get().ResidentSpan(address, bytes, start, end); +} + +void GuestArenaForgetSection_nid_postfix(void* section) { + WindowsMappings::Get().ForgetSection(section); +} #endif #if defined(__linux__) diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index b2ae0d13ae..d35ccdb451 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -52,7 +52,7 @@ public: if (total > MaximumSmallBytes) { blockBytes = alignUp(total, LargeGranuleBytes); if (void* cached = _large.Take(blockBytes)) return cached; - void* raw = GuestArena::GuestArenaAllocate_nid_postfix(blockBytes, PageBytes); + void* raw = arenaAllocate(blockBytes, PageBytes); commit(raw, blockBytes); return raw; } @@ -65,7 +65,7 @@ public: return block; } if (sizeClass.bump == nullptr || sizeClass.bytes > static_cast(sizeClass.bumpEnd - sizeClass.bump)) { - auto* span = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SpanBytes, PageBytes)); + auto* span = static_cast(arenaAllocate(SpanBytes, PageBytes)); commit(span, SpanBytes); sizeClass.bump = span; sizeClass.bumpEnd = span + SpanBytes; @@ -111,8 +111,42 @@ private: return static_cast(found - _classes.begin()); } + static void* arenaAllocate(std::size_t bytes, std::size_t alignment) { + try { + return GuestArena::GuestArenaAllocate_nid_postfix(bytes, alignment); + } catch (const std::runtime_error&) { + throw std::bad_alloc(); + } + } + static void commit(void* pointer, std::size_t bytes) { - GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); + try { + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, ReadWriteProtection, bytes); + } catch (const std::system_error& error) { + abandon(pointer, bytes); + if (outOfMemory(error)) throw std::bad_alloc(); + throw; + } catch (...) { + abandon(pointer, bytes); + throw; + } + } + + static void abandon(void* pointer, std::size_t bytes) { + GuestArena::GuestArenaReset_nid_postfix(pointer, bytes); + GuestArena::GuestArenaRelease_nid_postfix(pointer, bytes); + } + + static bool outOfMemory(const std::system_error& error) { +#ifdef _WIN32 + constexpr int NotEnoughMemory = 8; + constexpr int OutOfMemory = 14; + constexpr int CommitmentLimit = 1455; + return error.code().category() == std::system_category() && + (error.code().value() == NotEnoughMemory || error.code().value() == OutOfMemory || error.code().value() == CommitmentLimit); +#else + return error.code() == std::errc::not_enough_memory; +#endif } // Freed large blocks stay committed and are handed out again for the same block size: the game diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 1a835e2fe8..ac5bcac781 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -18,7 +18,7 @@ namespace { template std::int64_t RoundToInt64(TFloat value) { const TFloat rounded = std::round(value); - if (!(rounded >= static_cast(INT64_MIN) && rounded < static_cast(INT64_MAX))) { + if (!(rounded >= static_cast(INT64_MIN) && rounded < -static_cast(INT64_MIN))) { std::feraiseexcept(FE_INVALID); return INT64_MIN; } @@ -465,6 +465,19 @@ short APS5_VABI _FDtest_nid_postfix(const float* value) { if (exponent == 0) return fraction != 0 ? Denormal : Zero; return Finite; } + +short APS5_VABI _Dtest_nid_postfix(const double* value) { + constexpr short Denormal = -2, Finite = -1, Zero = 0, Infinite = 1, NotANumber = 2; + if (value == nullptr) throw std::invalid_argument("_Dtest: null value"); + std::uint64_t bits; + std::memcpy(&bits, value, sizeof(bits)); + const auto exponent = bits & 0x7ff0000000000000u; + const auto fraction = bits & 0x000fffffffffffffu; + if (exponent == 0x7ff0000000000000u) return fraction != 0 ? NotANumber : Infinite; + if (exponent == 0) return fraction != 0 ? Denormal : Zero; + return Finite; +} + int APS5_VABI __isnanf_nid_postfix(float x) { return std::isnan(x) ? 1 : 0; } int APS5_VABI isnanf_nid_postfix(float x) { return __isnanf_nid_postfix(x); } int APS5_VABI isnan_nid_postfix(double x) { return __isnan_nid_postfix(x); } @@ -526,4 +539,13 @@ int APS5_VABI _Fltrounds_nid_postfix(void) { return SonyFltrounds(); } +union SonyDconst { + unsigned short _Word[8]; + float _Float; + double _Double; + long double _Long_double; +}; + +static_assert(std::endian::native == std::endian::little); +SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; } diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index be9746f2b7..143d50eba2 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -261,6 +261,10 @@ void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void* handle, std::size_t alig return Allocate(Find(handle), size, alignment); } +void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t alignment, std::size_t size) { + return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); +} + void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) { if (alignment == 0 || (alignment & (alignment - 1))) { Error(22); diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index fd810843e1..76a702a2a6 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -33,6 +33,7 @@ std::stop_source shutdownSource; std::atomic exitRequested{false}; std::atomic exitStarted{false}; using GuestExitCallback = void (APS5_VABI*)(); +thread_local bool runningExit = false; void APS5_VABI RunGuestExitCallback(void* callback) { reinterpret_cast(callback)(); @@ -115,8 +116,12 @@ extern "C" { [[noreturn]] void LibcExit_nid_no_patch(int code) { exitRequested.store(true); - if (exitStarted.exchange(true)) LibcAwaitExit_nid_postfix(); - LibcRunShutdown_nid_postfix(); + if (exitStarted.exchange(true)) { + if (!runningExit) LibcAwaitExit_nid_postfix(); + } else { + runningExit = true; + LibcRunShutdown_nid_postfix(); + } CxaFinalize_nid_no_patch(nullptr); std::exit(code); } diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 56a0f8d499..4b7ae591d7 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -192,4 +192,8 @@ unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, un return SonyUmodti3(dividend, divisor); } +void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { + NotImplemented_nid_no_patch(__func__); +} + } diff --git a/core/libs/prx/libc/src/ScanningWide.cpp b/core/libs/prx/libc/src/ScanningWide.cpp index 04917fdeba..e03bf6a2d4 100644 --- a/core/libs/prx/libc/src/ScanningWide.cpp +++ b/core/libs/prx/libc/src/ScanningWide.cpp @@ -13,6 +13,14 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/WindowsFormatting.hpp" +#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 + 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); @@ -122,15 +130,9 @@ Fit FloatFit(std::string_view text) { return Fit::Valid; } -std::size_t IntegerSize(const std::string& length) { - if (length.empty()) return sizeof(std::int32_t); - if (length == "hh") return sizeof(std::int8_t); - if (length == "h") return sizeof(std::int16_t); - if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); - throw std::invalid_argument("swscanf_s: invalid integer length modifier " + length); -} - class WideScanner { + const char* const name; + const bool sized; const char16_t* const start; const char16_t* input; const char16_t* format; @@ -139,8 +141,8 @@ class WideScanner { bool converted = false; public: - WideScanner(const char16_t* buffer, const char16_t* formatText, const void* source) - : start(buffer), input(buffer), format(formatText), args(source) {} + 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) {} int Run() { while (*format != 0) { @@ -162,6 +164,18 @@ public: } private: + [[noreturn]] void Fail(const std::string& message) const { + throw std::invalid_argument(std::string(name) + ": " + message); + } + + std::size_t IntegerSize(const std::string& length) const { + if (length.empty()) return sizeof(std::int32_t); + if (length == "hh") return sizeof(std::int8_t); + if (length == "h") return sizeof(std::int16_t); + if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") return sizeof(std::int64_t); + Fail("invalid integer length modifier " + length); + } + void SkipSpace() { while (*input != 0 && IsSpace(*input)) ++input; } @@ -185,7 +199,7 @@ private: const std::size_t limit = (std::numeric_limits::max() - digit) / 10; directive.width = directive.width > limit ? std::numeric_limits::max() : directive.width * 10 + digit; } - if (format != digits && directive.width == 0) throw std::invalid_argument("swscanf_s: zero field width"); + if (format != digits && directive.width == 0) Fail("zero field width"); if (*format == u'h' || *format == u'l') { directive.length += static_cast(*format++); if (*format == directive.length[0]) directive.length += static_cast(*format++); @@ -193,7 +207,7 @@ private: directive.length += static_cast(*format++); } directive.conversion = *format; - if (directive.conversion == 0) throw std::invalid_argument("swscanf_s: incomplete conversion specification"); + if (directive.conversion == 0) Fail("incomplete conversion specification"); ++format; if (directive.conversion == u'[') { directive.setExcluded = *format == u'^'; @@ -201,10 +215,10 @@ private: directive.setBegin = format; if (*format == u']') ++format; while (*format != 0 && *format != u']') ++format; - if (*format == 0) throw std::invalid_argument("swscanf_s: unterminated scanset"); + if (*format == 0) Fail("unterminated scanset"); directive.setEnd = format++; for (const char16_t* member = directive.setBegin + 1; member + 1 < directive.setEnd; ++member) { - if (*member == u'-') throw std::invalid_argument("swscanf_s: scanset range is implementation-defined"); + if (*member == u'-') Fail("scanset range is implementation-defined"); } } return directive; @@ -230,7 +244,7 @@ private: case u'c': case u'[': return Text(directive); case u'n': return Count(directive); - default: throw std::invalid_argument("swscanf_s: unsupported conversion " + std::to_string(directive.conversion)); + default: Fail("unsupported conversion " + std::to_string(directive.conversion)); } } @@ -271,7 +285,7 @@ private: 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") - throw std::invalid_argument("swscanf_s: invalid floating length modifier " + directive.length); + Fail("invalid floating length modifier " + directive.length); void* target = nullptr; if (!directive.suppress && (target = args.Next()) == nullptr) return Outcome::Violation; SkipSpace(); @@ -312,12 +326,12 @@ private: Outcome Text(const Directive& directive) { if (!directive.length.empty() && directive.length != "l") - throw std::invalid_argument("swscanf_s: invalid string length modifier " + directive.length); + Fail("invalid string length modifier " + directive.length); void* target = nullptr; std::size_t capacity = 0; if (!directive.suppress) { target = args.Next(); - capacity = args.Next(); + if (sized) capacity = args.Next(); if (target == nullptr) return Outcome::Violation; } if (directive.conversion == u's') SkipSpace(); @@ -343,7 +357,7 @@ private: } const std::size_t terminator = directive.conversion == u'c' ? 0 : 1; if (directive.length == "l") { - if (item.size() + terminator > capacity) return Truncated(target, capacity, true); + if (sized && item.size() + terminator > capacity) return Truncated(target, capacity, true); std::memcpy(target, item.data(), item.size() * sizeof(char16_t)); if (terminator != 0) static_cast(target)[item.size()] = 0; } else { @@ -351,10 +365,10 @@ private: for (const char16_t unit : item) { char byte[8]; const std::size_t written = wcrtomb_nid_postfix(byte, unit, nullptr); - if (written == static_cast(-1)) throw std::invalid_argument("swscanf_s: wide character has no single-byte form"); + if (written == static_cast(-1)) Fail("wide character has no single-byte form"); bytes.append(byte, written); } - if (bytes.size() + terminator > capacity) return Truncated(target, capacity, false); + if (sized && bytes.size() + terminator > capacity) return Truncated(target, capacity, false); std::memcpy(target, bytes.data(), bytes.size()); if (terminator != 0) static_cast(target)[bytes.size()] = 0; } @@ -364,7 +378,7 @@ private: } Outcome Count(const Directive& directive) { - if (directive.suppress || directive.width != 0) throw std::invalid_argument("swscanf_s: %n with suppression or width"); + if (directive.suppress || directive.width != 0) Fail("%n with suppression or width"); const std::size_t size = IntegerSize(directive.length); void* target = args.Next(); if (target == nullptr) return Outcome::Violation; @@ -378,29 +392,28 @@ private: extern "C" { -int APS5_VABI swscanf_s_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { +int APS5_VABI swscanf_nid_postfix(const char16_t* buffer, const char16_t* format, ...) { if (buffer == nullptr || format == nullptr) return EOF; -#ifdef _WIN32 - __builtin_sysv_va_list args; - __builtin_sysv_va_start(args, format); -#else - std::va_list args; - va_start(args, format); -#endif + APS5_VA_BEGIN(format); try { - const int result = WideScanner(buffer, format, args).Run(); -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + const int result = WideScanner("swscanf", false, buffer, format, args).Run(); + APS5_VA_END(); return result; } catch (...) { -#ifdef _WIN32 - __builtin_sysv_va_end(args); -#else - va_end(args); -#endif + APS5_VA_END(); + throw; + } +} + +int APS5_VABI swscanf_s_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_s", true, buffer, format, args).Run(); + APS5_VA_END(); + return result; + } catch (...) { + APS5_VA_END(); throw; } } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index e74bcb5f95..b1d85f9faf 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -294,4 +294,10 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { } } + +int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { + return SignalCondition_nid_no_patch(handle); } + +} + diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 056d0ab413..b782ef1549 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -402,10 +402,22 @@ std::size_t formatGuestTime(char* destination, std::size_t size, const char* for *buffer.cursor = '\0'; return static_cast(buffer.cursor - destination); } + +char** HostZoneNames() { +#ifdef _WIN32 + _tzset(); + return _tzname; +#else + ::tzset(); + return ::tzname; +#endif +} } extern "C" { +char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; + 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); std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state); @@ -413,11 +425,9 @@ std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const ch int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { -#ifdef _WIN32 - _tzset(); -#else - ::tzset(); -#endif + char** names = HostZoneNames(); + tzname_nid_postfix[0] = names[0]; + tzname_nid_postfix[1] = names[1]; } int64_t APS5_VABI libc_time_nid_postfix(int64_t* timer) { diff --git a/core/libs/prx/libc/src/exception/TypeInfo.cpp b/core/libs/prx/libc/src/exception/TypeInfo.cpp index c745eda722..49ba0e47ee 100644 --- a/core/libs/prx/libc/src/exception/TypeInfo.cpp +++ b/core/libs/prx/libc/src/exception/TypeInfo.cpp @@ -13,6 +13,8 @@ void APS5_VABI DeleteTypeInfo(std::type_info*) { } bool APS5_VABI IsPointerTypeInfo(const std::type_info*) { return false; } +bool APS5_VABI IsPointerTypeInfoTrue(const std::type_info*) { return true; } +bool APS5_VABI IsFunctionTypeInfoTrue(const std::type_info*) { return true; } bool APS5_VABI CatchTypeInfo(const std::type_info* self, const std::type_info* thrown, void** object, unsigned) { if (!self || !thrown || !object) throw std::invalid_argument("invalid RTTI catch arguments"); @@ -65,4 +67,19 @@ extern "C" { LibcException::TypeInfoVtable _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__si_class_type_info)}; LibcException::TypeInfoVtable _ZTVN10__cxxabiv121__vmi_class_type_infoE_nid_postfix {0, &typeid(__cxxabiv1::__vmi_class_type_info)}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv119__pointer_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__pointer_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfoTrue +}; +LibcException::TypeInfoVtable _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix { + 0, + &typeid(__cxxabiv1::__function_type_info), + LibcException::DestroyTypeInfo, + LibcException::DeleteTypeInfo, + LibcException::IsPointerTypeInfo, + LibcException::IsFunctionTypeInfoTrue +}; } diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index a4671a43eb..6491da364c 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -3,6 +3,7 @@ #include #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -226,6 +227,50 @@ void TraceSse4a(std::uint64_t rip, const sse4a::Instruction& instruction, const Report("[sse4a] #%llu %s %s (length %u, index %u, %u bytes) at %s on thread %lu\n", count, mnemonic, operands, field.length, field.index, static_cast(instruction.size), line, GetCurrentThreadId()); } +constexpr DWORD GccExceptionMask = 0x20ffffffu; +constexpr DWORD GccException = 0x20474343u; +constexpr std::uint64_t GccExceptionClass = 0x474e5543432b2b00ull; +constexpr std::uint64_t CxaExceptionTypeOffset = 80; + +std::uint64_t FrameWithoutUnwindData(const CONTEXT& start) { + const HMODULE runtime[] = {GetModuleHandleA("KERNELBASE.dll"), GetModuleHandleA("libgcc_s_seh-1.dll"), GetModuleHandleA("libstdc++-6.dll")}; + CONTEXT context = start; + for (int depth = 0; depth < 64 && context.Rip != 0; ++depth) { + HMODULE module = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(context.Rip), &module); + DWORD64 imageBase = 0; + const auto entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr); + if (module == nullptr || std::find(std::begin(runtime), std::end(runtime), module) == std::end(runtime)) return entry == nullptr ? context.Rip : 0; + if (entry == nullptr) return 0; + PVOID handlerData = nullptr; + DWORD64 establisher = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handlerData, &establisher, nullptr); + } + return 0; +} + +bool ReadWord(std::uint64_t address, std::uint64_t* value) { + return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); +} + +void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { + const auto* record = info->ExceptionRecord; + char type[256] = "of unknown type"; + const auto header = record->NumberParameters >= 1 ? static_cast(record->ExceptionInformation[0]) : 0; + std::uint64_t exceptionClass = 0; + std::uint64_t typeInfo = 0; + std::uint64_t namePointer = 0; + if (ReadWord(header, &exceptionClass) && exceptionClass == GccExceptionClass && ReadWord(header - CxaExceptionTypeOffset, &typeInfo) && ReadWord(typeInfo + 8, &namePointer)) { + std::uint8_t name[sizeof(type)]; + const auto copied = ReadCode(namePointer, name, sizeof(name)); + if (const auto* end = static_cast(std::memchr(name, 0, copied))) std::memcpy(type, name, static_cast(end - name) + 1); + } + char line[MAX_PATH + 64]; + DescribeAddress(frame, line, sizeof(line)); + Report("\nFATAL: C++ exception %s raised on thread %lu cannot be delivered: %s has no unwind data\n", type, GetCurrentThreadId(), line); + ReportStackCandidates(info->ContextRecord->Rsp, 32, 0x4000); +} + bool HandleSse4a(EXCEPTION_POINTERS* info) { if (!g_sse4aEmulation) return false; const auto* record = info->ExceptionRecord; @@ -246,9 +291,14 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; const auto* fault = info->ExceptionRecord; if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; + if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 0 && GuestArena::GuestArenaHandleRead_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; + if ((record->ExceptionCode & GccExceptionMask) == GccException) { + if (const auto frame = FrameWithoutUnwindData(*info->ContextRecord)) ReportUndeliverableException(info, frame); + return EXCEPTION_CONTINUE_SEARCH; + } if (!IsFatal(record->ExceptionCode)) return EXCEPTION_CONTINUE_SEARCH; if (reported.exchange(true)) { // Another thread is already reporting; let it finish before this fault ends the process. @@ -282,11 +332,24 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { Report(" r12 %016llx r13 %016llx r14 %016llx r15 %016llx\n", context->R12, context->R13, context->R14, context->R15); // The words each register points at, to recognise which structure a bad pointer came from. { + const DWORD indirectLength = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); + const bool indirectMemory = indirectLength > 1; const std::pair pointers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; for (const auto& [name, value] : pointers) { if (value < 0x10000 || (value & 7) != 0 || !IsReadable(value) || !IsReadable(value + 0x38)) continue; const auto* words = reinterpret_cast(value); Report(" [%s] %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", name, words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + if (!indirectMemory) continue; + for (std::size_t i = 0; i < 8; ++i) { + const auto address = words[i]; + if (address < 0x10000 || IsExecutable(address)) continue; + std::uint64_t content[8]{}; + SIZE_T copied = 0; + if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(address), content, sizeof(content), &copied) || copied != sizeof(content)) continue; + Report(" [[%s+0x%llx]] @%016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", + name, static_cast(i * sizeof(*words)), static_cast(address), + content[0], content[1], content[2], content[3], content[4], content[5], content[6], content[7]); + } } } Report(" frame pointer chain:\n"); diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index be912c6e78..22f615c522 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -3,6 +3,7 @@ #include #include +#include #include struct ParsedParamJson { @@ -10,6 +11,7 @@ struct ParsedParamJson { std::string titleId; std::uint64_t downloadDataSizeMiB = 0; std::uint32_t attribute3 = 0; + std::optional flexibleMemorySizeBytes; std::int32_t userDefinedParams[4] = {}; }; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 585b732a1d..8cde56681f 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include #include #include +#include #include #include #include @@ -206,6 +208,21 @@ JsonValue parseJsonArray(const std::string& text, std::size_t& position, std::si return value; } +double parseJsonNumberValue(const std::string& number) { + double value = 0.0; +#ifdef __cpp_lib_to_chars + const auto converted = std::from_chars(number.data(), number.data() + number.size(), value); + const bool parsed = converted.ec == std::errc{} && converted.ptr == number.data() + number.size(); +#else + std::istringstream stream(number); + stream.imbue(std::locale::classic()); + stream >> value; + const bool parsed = !stream.fail() && stream.eof(); +#endif + if (!parsed) throw std::runtime_error("number outside supported range in param.json"); + return value; +} + JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { const std::size_t start = position; if (peekChar(text, position) == '-') ++position; @@ -230,10 +247,7 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { JsonValue value; value.type = JsonType::Number; value.numberText = text.substr(start, position - start); - const auto& number = value.numberText; - const auto converted = std::from_chars(number.data(), number.data() + number.size(), value.numberValue); - if (converted.ec != std::errc{} || converted.ptr != number.data() + number.size()) - throw std::runtime_error("number outside supported range in param.json"); + value.numberValue = parseJsonNumberValue(value.numberText); return value; } @@ -314,6 +328,20 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { const auto parsed = std::from_chars(number.data(), number.data() + number.size(), result.attribute3); if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json attribute3 exceeds uint32 range"); } + if (const JsonValue* kernel = findObjectMember(root, "kernel")) { + if (const JsonValue* flexible = findObjectMember(*kernel, "flexibleMemorySize")) { + const auto& number = flexible->numberText; + if (flexible->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) + throw std::runtime_error("param.json kernel.flexibleMemorySize is not an unsigned integer"); + std::uint64_t bytes = 0; + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), bytes); + if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds uint64 range"); + if (bytes == 0 || (bytes & (PS5_PAGE_SIZE - 1)) != 0) + throw std::runtime_error("param.json kernel.flexibleMemorySize must be a positive multiple of 16 KiB"); + result.flexibleMemorySizeBytes = bytes; + } + } if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); if (downloadData->numberText.find_first_of("-.eE") == std::string::npos) { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 57199523e2..22e9358fc2 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -168,7 +168,8 @@ void _readFile(const Apr::ReadFileCommand& command) { stream.read(reinterpret_cast(command.destination), static_cast(command.size)); if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); const auto read = static_cast(stream.gcount()); - if (read != command.size) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); + if (read != command.size && (!stream.eof() || command.offset + read < std::filesystem::file_size(file.path))) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); + if (read != command.size) std::memset(reinterpret_cast(command.destination) + read, 0, static_cast(command.size - read)); } void _writeAddress(const Apr::WriteAddressCommand& command) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f3fb4d0806..fbd7e7734e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Rwlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/src/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Atfork.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Attr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Barrier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Cond.cpp @@ -43,6 +44,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp @@ -57,6 +59,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/HostCpu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Login.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SoftwareVersion.cpp @@ -203,6 +206,18 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endforeach() endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_dynamic_loader_fixed_base_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp) + target_include_directories(guest_dynamic_loader_fixed_base_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_loader_fixed_base_tests PRIVATE libkernel libc) + add_dependencies(guest_dynamic_loader_fixed_base_tests guest_module_fixture) + set_target_properties(guest_dynamic_loader_fixed_base_tests PROPERTIES ENABLE_EXPORTS ON POSITION_INDEPENDENT_CODE OFF) + target_compile_options(guest_dynamic_loader_fixed_base_tests PRIVATE -fno-pie) + target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie) + add_test(NAME guest_dynamic_loader_fixed_base COMMAND guest_dynamic_loader_fixed_base_tests $) + set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES + WORKING_DIRECTORY "$") + 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) @@ -291,6 +306,15 @@ if(BUILD_TESTING) set_tests_properties(guest_aio PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_positioned_race_tests ${LIBS_INCLUDE_DIR}/tests/GuestPositionedRace.cpp) + target_include_directories(guest_positioned_race_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_positioned_race_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_positioned_race_tests) + add_test(NAME guest_positioned_race COMMAND guest_positioned_race_tests) + if(WIN32) + set_tests_properties(guest_positioned_race PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestOnce.cpp) target_include_directories(guest_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_once_tests PRIVATE libkernel libc) @@ -651,6 +675,15 @@ if(BUILD_TESTING) set_tests_properties(guest_parent_process PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_atfork_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAtfork.cpp) + target_include_directories(guest_pthread_atfork_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_atfork_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_atfork_tests) + add_test(NAME guest_pthread_atfork COMMAND guest_pthread_atfork_tests) + if(WIN32) + set_tests_properties(guest_pthread_atfork PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kill_tests ${LIBS_INCLUDE_DIR}/tests/GuestKill.cpp) target_include_directories(guest_kill_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kill_tests PRIVATE libkernel libc) @@ -955,6 +988,15 @@ if(BUILD_TESTING) set_tests_properties(guest_jit_shared_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(host_cpu_tests ${LIBS_INCLUDE_DIR}/tests/HostCpu.cpp) + target_include_directories(host_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(host_cpu_tests PRIVATE libkernel libc) + configure_windows_unwind(host_cpu_tests) + add_test(NAME host_cpu COMMAND host_cpu_tests) + if(WIN32) + set_tests_properties(host_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_configuration_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemConfiguration.cpp) target_include_directories(guest_system_configuration_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_configuration_tests PRIVATE libkernel libc) @@ -995,6 +1037,16 @@ if(BUILD_TESTING) target_include_directories(guest_socket_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_socket_error_tests) add_test(NAME guest_socket_error COMMAND guest_socket_error_tests) + add_test_executable(guest_socket_options_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketOptions.cpp) + target_include_directories(guest_socket_options_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_options_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_options_tests) + add_test(NAME guest_socket_options COMMAND guest_socket_options_tests) + set_tests_properties(guest_socket_options PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_socket_options PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) @@ -1026,13 +1078,25 @@ if(BUILD_TESTING) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) - target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_select_tests PRIVATE libkernel libc) - configure_windows_unwind(guest_select_tests) - add_test(NAME guest_select COMMAND guest_select_tests) - set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + add_test_executable(guest_select_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelectWait.cpp) + target_include_directories(guest_select_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_wait_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_wait_tests) + add_test(NAME guest_select_wait COMMAND guest_select_wait_tests) + set_tests_properties(guest_select_wait PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select_wait PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) + target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_tests) + add_test(NAME guest_select COMMAND guest_select_tests) + set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_select PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_tcp_tests ${LIBS_INCLUDE_DIR}/tests/GuestTcp.cpp) target_include_directories(guest_tcp_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -1131,11 +1195,57 @@ if(BUILD_TESTING) set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") add_dependencies(guest_memory_tests guest_memory_module) target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") + if(WIN32) + add_library(guest_memory_elf0_module SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.S) + set_target_properties(guest_memory_elf0_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_elf0_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_ELF0_MODULE="$") + endif() add_test(NAME guest_memory COMMAND guest_memory_tests) if(WIN32) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_flexible_memory_budget_tests ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryBudget.cpp) + target_include_directories(guest_flexible_memory_budget_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_flexible_memory_budget_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_flexible_memory_budget_tests) + add_library(guest_flexible_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestFlexibleMemoryModule.cpp) + set_target_properties(guest_flexible_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_flexible_memory_budget_tests guest_flexible_memory_module) + target_compile_definitions(guest_flexible_memory_budget_tests PRIVATE GUEST_FLEXIBLE_MEMORY_MODULE="$") + foreach(flexibleMemoryCase IN ITEMS declared:838860800 limited:16777216 none:0 missing:0 invalid:true) + string(REPLACE ":" ";" flexibleMemoryCase ${flexibleMemoryCase}) + list(GET flexibleMemoryCase 0 flexibleMemoryName) + list(GET flexibleMemoryCase 1 flexibleMemorySize) + set(flexibleMemoryRun ${CMAKE_CURRENT_BINARY_DIR}/guest_flexible_memory_${flexibleMemoryName}) + file(MAKE_DIRECTORY ${flexibleMemoryRun}) + if(NOT flexibleMemoryName STREQUAL "missing") + set(flexibleMemoryField "") + if(NOT flexibleMemoryName STREQUAL "none") + set(flexibleMemoryField ",\"kernel\":{\"flexibleMemorySize\":${flexibleMemorySize}}") + endif() + file(WRITE ${flexibleMemoryRun}/app0/sce_sys/param.json + "{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}${flexibleMemoryField}}") + endif() + add_test(NAME guest_flexible_memory_${flexibleMemoryName} COMMAND guest_flexible_memory_budget_tests ${flexibleMemoryName} + WORKING_DIRECTORY ${flexibleMemoryRun}) + if(WIN32) + set_tests_properties(guest_flexible_memory_${flexibleMemoryName} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_page_table_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestPageTableStats.cpp) + target_include_directories(guest_page_table_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_page_table_stats_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_page_table_stats_tests) + add_test(NAME guest_page_table_stats COMMAND guest_page_table_stats_tests) + if(WIN32) + set_tests_properties(guest_page_table_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_mlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestMlock.cpp) target_include_directories(guest_mlock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mlock_tests PRIVATE libkernel libc) @@ -1245,6 +1355,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_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) + configure_windows_unwind(guest_mutex_contention_tests) + add_test(NAME guest_mutex_contention COMMAND guest_mutex_contention_tests) + set_tests_properties(guest_mutex_contention PROPERTIES TIMEOUT 20 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadRename.cpp) target_include_directories(guest_pthread_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index a35b55b556..a328556d73 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -302,6 +302,7 @@ public: ~PhysicalBacking() { #ifdef _WIN32 + GuestArena::GuestArenaForgetSection_nid_postfix(section); CloseHandle(section); #else if (hostWriteView != nullptr) ::munmap(hostWriteView, bytes); @@ -313,7 +314,7 @@ public: void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { #ifdef _WIN32 - GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); + GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); #else if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); #endif @@ -552,11 +553,44 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int #endif } +void RequireResidentUnpinned(GuestAllocations::Mutation& mutation, std::uintptr_t address, std::size_t len) { +#ifdef _WIN32 + std::uintptr_t start = 0; + std::uintptr_t end = 0; + if (GuestArena::GuestArenaResidentSpan_nid_postfix(address, len, &start, &end)) mutation.RequireUnpinned(reinterpret_cast(start), end - start); +#else + static_cast(mutation); + static_cast(address); + static_cast(len); +#endif +} + +void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t phys, std::size_t len) { +#ifdef _WIN32 + std::vector> aliases; + { + std::lock_guard lock(g_directLock); + for (const auto& [base, mapping] : g_directMappings) { + 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); + } + } + for (const auto& [address, bytes] : aliases) RequireResidentUnpinned(mutation, address, bytes); +#else + static_cast(mutation); + static_cast(phys); + static_cast(len); +#endif +} + bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; if (addr == nullptr || (flags & GuestMapFixed) == 0 || (flags & GuestMapNoOverwrite) != 0 || !mutation.Covers(addr, len)) return false; ValidateRange(addr, len, PS5_PAGE_SIZE); + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); + if (physStart >= 0) RequireAliasesUnpinned(mutation, static_cast(physStart), len); Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast(physStart)); const auto nativeProtection = LinuxProtFromSce(prot); mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { @@ -798,6 +832,7 @@ bool Reserved(const void* addr, size_t len) { } void UnmapRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len) { + RequireResidentUnpinned(mutation, reinterpret_cast(addr), len); mutation.Unmap(addr, len, [&](const void* piece, std::size_t pieceBytes, const void* allocation, bool last) { auto* pieceAddress = const_cast(piece); std::lock_guard lock(g_directLock); @@ -847,6 +882,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return 0; } ReplaceFixedOverlap(mutation, *addr, len, flags); + RequireAliasesUnpinned(mutation, static_cast(physStart), len); std::lock_guard lock(g_directLock); ValidatePhysicalRange(static_cast(physStart), len); void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment); @@ -957,19 +993,17 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { #ifdef _WIN32 static bool IsGuestModuleImage(const void* base) { - std::wstring path(MAX_PATH, L'\0'); - for (;;) { - const auto length = GetModuleFileNameW(static_cast(const_cast(base)), path.data(), static_cast(path.size())); - if (length == 0) return false; - if (length < path.size()) { - path.resize(length); - break; - } - path.resize(path.size() * 2); + const auto address = reinterpret_cast(base); + const auto* dos = reinterpret_cast(address); + if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) return false; + const auto* nt = reinterpret_cast(address + static_cast(dos->e_lfanew)); + if (nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return false; + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + for (unsigned i = 0; i < sectionCount; ++i) { + if (std::strncmp(reinterpret_cast(sections[i].Name), ".elf0", IMAGE_SIZEOF_SHORT_NAME) == 0) return true; } - constexpr std::wstring_view suffix = L".guest.prx"; - if (path.size() < suffix.size()) return false; - return std::equal(suffix.begin(), suffix.end(), path.end() - static_cast(suffix.size()), [](wchar_t expected, wchar_t actual) { return std::towlower(actual) == expected; }); + return false; } #endif @@ -1134,6 +1168,20 @@ bool GuestProtection(uintptr_t addr, int* prot) { return true; } +void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpuEntries, std::uint64_t* gpuEntries) { + std::uint64_t cpu = 0; + std::uint64_t gpu = 0; + std::lock_guard lock(g_protectionLock); + for (const auto& [start, range] : g_protections) { + if (range.end <= start) continue; + const auto entries = (range.end - 1) / granularity - start / granularity + 1; + if (range.prot & 0x7) cpu += entries; + if (range.prot & GuestProtGpuReadWrite) gpu += entries; + } + *cpuEntries = cpu; + *gpuEntries = gpu; +} + bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType) { std::lock_guard lock(g_directLock); const auto next = g_directMappings.upper_bound(address); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index c611faeabf..0307c36cab 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -47,5 +47,6 @@ bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end); void ForgetJitSharedMemory(int descriptor); void DuplicateJitSharedMemory(int from, int to); +void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpu_entries, std::uint64_t* gpu_entries); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index ea7e00b055..dca35ab051 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -36,7 +37,18 @@ extern "C" int* APS5_VABI __error_nid_postfix(); namespace { -constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; +size_t _configuredFlexibleSize() { + static const size_t size = [] { + constexpr std::uint64_t defaultSize = 448ULL * 1024 * 1024; + const auto path = ResolvePath_nid_no_patch("/app0/sce_sys/param.json"); + const auto bytes = std::filesystem::exists(path) ? parseParamJson(path).flexibleMemorySizeBytes.value_or(defaultSize) : defaultSize; + if (bytes > std::numeric_limits::max()) + throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds host size range"); + std::fprintf(stderr, "[memory] configured flexible memory: 0x%llx bytes (%llu MiB)\n", static_cast(bytes), static_cast(bytes >> 20)); + return static_cast(bytes); + }(); + return size; +} constexpr int PRT_APERTURE_COUNT = 3; @@ -94,11 +106,12 @@ size_t _flexibleUsedLocked() { } int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t configured = _configuredFlexibleSize(); const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); const size_t used = images + _flexibleUsedLocked(); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, used)) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, static_cast(FLEXIBLE_MEMORY_SIZE)); + if (len > configured - std::min(configured, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); return SCE_KERNEL_ERROR_ENOMEM; } const int result = DoMapAnon(addr, len, prot, flags); @@ -360,15 +373,16 @@ int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; + const size_t configured = _configuredFlexibleSize(); const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, images + _flexibleUsedLocked()); + *size = configured - std::min(configured, images + _flexibleUsedLocked()); return 0; } int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; - *size = FLEXIBLE_MEMORY_SIZE; + *size = _configuredFlexibleSize(); return 0; } @@ -397,11 +411,15 @@ int APS5_VABI sceKernelClearVirtualRangeName(const void* addr, uint64_t len) { int APS5_VABI sceKernelGetPageTableStats(int* cpu_total, int* cpu_available, int* gpu_total, int* gpu_available) { if (!cpu_total || !cpu_available || !gpu_total || !gpu_available) return SCE_KERNEL_ERROR_EFAULT; - constexpr int pageTableEntries = static_cast((4ull << 30) / (2ull << 20)); - *cpu_total = pageTableEntries; - *cpu_available = pageTableEntries; - *gpu_total = pageTableEntries; - *gpu_available = pageTableEntries; + constexpr std::uint64_t PageTableGranularity = 2ULL * 1024 * 1024; + constexpr std::uint64_t PageTablePoolEntries = 2048; + std::uint64_t cpuUsed = 0; + std::uint64_t gpuUsed = 0; + GuestPageTableUsage(PageTableGranularity, &cpuUsed, &gpuUsed); + *cpu_total = static_cast(PageTablePoolEntries); + *gpu_total = static_cast(PageTablePoolEntries); + *cpu_available = static_cast(cpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - cpuUsed); + *gpu_available = static_cast(gpuUsed >= PageTablePoolEntries ? 0 : PageTablePoolEntries - gpuUsed); return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp index ca31b70ce9..fb3ad0405c 100644 --- a/core/libs/prx/libkernel/DirectMemory/Jit.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -62,7 +62,10 @@ class JitObject { public: #ifdef _WIN32 JitObject(std::size_t bytes, HANDLE section) : bytes(bytes), section(section) {} - ~JitObject() { CloseHandle(section); } + ~JitObject() { + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); + } #else explicit JitObject(std::size_t bytes) : bytes(bytes) {} #endif diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index d7c6c65d76..8fa22b5fbc 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,10 +1,54 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include +#include +#include #include #include +#include +#include +#include -static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; +#include "prx/libkernel/KernelErrors.hpp" + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPoolBytes = static_cast(DIRECT_MEMORY_SIZE); +constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); + +static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); +static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); + +bool RoundUp(uint64_t v, uint64_t a, uint64_t& out) { + const uint64_t r = v % a; + if (r == 0) { + out = v; + return true; + } + if (v > UINT64_MAX - (a - r)) return false; + out = v + (a - r); + return true; +} + +bool NormalizeAlignment(size_t alignment, uint64_t& out) { + return RoundUp(std::max(static_cast(alignment), kPage), kPage, out); +} + +bool NormalizeLength(size_t len, uint64_t& out) { + uint64_t rounded; + if (!RoundUp(static_cast(len), kPage, rounded) || rounded > kPoolBytes) return false; + out = rounded; + return true; +} + +bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { + if (len == 0 || start >= kPoolBytes || len > kPoolBytes - start) return false; + lo = start & ~(kPage - 1); + hi = (start + len + kPage - 1) & ~(kPage - 1); + return true; +} struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { @@ -13,87 +57,98 @@ struct PhysicalMemoryPool { } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + if (len == 0) return SCE_KERNEL_ERROR_EINVAL; + + uint64_t align, alignedLen; + if (!NormalizeAlignment(alignment, align) || !NormalizeLength(len, alignedLen)) return SCE_KERNEL_ERROR_EAGAIN; + + const uint64_t start = static_cast(searchStart); + const uint64_t end = std::min(static_cast(searchEnd), kPoolBytes); + uint64_t cur; + if (start >= end || !RoundUp(start, align, cur)) return SCE_KERNEL_ERROR_EAGAIN; + std::lock_guard lock(_mutex); - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - uint64_t start = static_cast(searchStart); - uint64_t end = static_cast(searchEnd); - uint64_t cur = (start + align - 1) & ~(align - 1); - while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { - if (_isFree(cur, len)) { - CreateDirectMemoryBacking(static_cast(cur), len, memoryType); - _mark(cur, len, true); - _blocks[cur] = {cur + len, memoryType}; + const size_t pageCount = static_cast(alignedLen / kPage); + + while (cur <= end && alignedLen <= end - cur) { + size_t lastUsed; + if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { + _commit(cur, alignedLen, memoryType); *physOut = static_cast(cur); return 0; } - cur += align; + if (!RoundUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } return SCE_KERNEL_ERROR_EAGAIN; } - void Free(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - _free(start, len); - } + bool Free(uint64_t start, size_t len) { return _release(start, len, false); } - bool CheckedFree(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; - const size_t first = start / PS5_PAGE_SIZE; - for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) - if (!_used[first + i]) return false; - _free(start, len); - return true; - } + bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } 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); - auto it = _blocks.upper_bound(offset); + BlockMap::const_iterator it = _blocks.upper_bound(offset); if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); *end = static_cast(it->second.end); *memoryType = it->second.type; return true; } - void Retype(uint64_t start, size_t len, int memoryType) { + bool Retype(uint64_t start, size_t len, int memoryType) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + std::lock_guard lock(_mutex); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + const bool splitLo = _splitAt(lo); + try { + _splitAt(hi); + } catch (...) { + if (splitLo) _unsplitAt(lo); + throw; } + for (BlockMap::iterator it = _blocks.lower_bound(lo); it != _blocks.end() && it->first < hi; ++it) { + it->second.type = memoryType; + } + return true; } bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - std::lock_guard lock(_mutex); + if (!startOut || !sizeOut) return false; if (searchStart < 0 || searchEnd <= searchStart) return false; - const uint64_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - const uint64_t limit = std::min(static_cast(searchEnd), DIRECT_MEMORY_SIZE); + uint64_t align; + if (!NormalizeAlignment(alignment, align)) return false; + + uint64_t cur; + if (!RoundUp(static_cast(searchStart), kPage, cur)) return false; + const uint64_t limit = std::min(static_cast(searchEnd), kPoolBytes) & ~(kPage - 1); + + std::lock_guard lock(_mutex); uint64_t bestStart = 0; uint64_t bestSize = 0; - uint64_t cur = (static_cast(searchStart) + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - while (cur + PS5_PAGE_SIZE <= limit) { - if (!_isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; + + while (cur < limit) { + if (_used[cur / kPage]) { + cur += kPage; continue; } uint64_t runEnd = cur; - while (runEnd + PS5_PAGE_SIZE <= limit && _isFree(runEnd, PS5_PAGE_SIZE)) runEnd += PS5_PAGE_SIZE; - const uint64_t alignedStart = (cur + align - 1) & ~(align - 1); + while (runEnd < limit && !_used[runEnd / kPage]) runEnd += kPage; + + uint64_t alignedStart; + if (!RoundUp(cur, align, alignedStart)) break; if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { bestStart = alignedStart; bestSize = runEnd - alignedStart; } cur = runEnd; } + if (bestSize == 0) return false; *startOut = static_cast(bestStart); *sizeOut = static_cast(bestSize); @@ -101,64 +156,122 @@ struct PhysicalMemoryPool { } size_t FreeRun(uint64_t offset, uint64_t limit) { + const uint64_t lim = std::min(limit, kPoolBytes); + if (offset >= lim) return 0; + std::lock_guard lock(_mutex); - uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); + uint64_t cur = offset & ~(kPage - 1); size_t run = 0; - while (cur + PS5_PAGE_SIZE <= limit && cur + PS5_PAGE_SIZE <= DIRECT_MEMORY_SIZE - && _isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; + while (cur <= lim && kPage <= lim - cur && !_used[cur / kPage]) { + cur += kPage; + run += static_cast(kPage); } return run; } private: - void _free(uint64_t start, size_t len) { - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; - } - } - - bool _isFree(uint64_t offset, size_t len) const { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - if (_used[first + i]) return false; - return true; - } - - void _mark(uint64_t offset, size_t len, bool used) { - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) - _used[first + i] = used; - } - struct Block { uint64_t end; int type; }; + typedef std::map BlockMap; + + bool _lastUsedPage(size_t first, size_t count, size_t& out) const { + for (size_t i = count; i-- > 0;) { + if (_used[first + i]) { + out = first + i; + return true; + } + } + return false; + } + + bool _allUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (!_used[first + i]) return false; + } + return true; + } + + void _mark(size_t first, size_t count, bool used) { + for (size_t i = 0; i < count; ++i) _used[first + i] = used; + } + + void _commit(uint64_t start, uint64_t len, int type) { + std::pair ins = _blocks.emplace(start, Block{start + len, type}); + if (!ins.second) throw std::logic_error("DirectMemory pool: block map and page flags disagree"); + + try { + CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); + } catch (...) { + _blocks.erase(ins.first); + throw; + } + + _mark(static_cast(start / kPage), static_cast(len / kPage), true); + } + + bool _release(uint64_t start, size_t len, bool requireAllocated) { + uint64_t lo, hi; + if (!PageSpan(start, len, lo, hi)) return false; + 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); + bool splitHi = false; + try { + splitHi = _splitAt(hi); + ForgetDirectMemory(static_cast(lo), static_cast(hi - lo)); + } catch (...) { + if (splitHi) _unsplitAt(hi); + if (splitLo) _unsplitAt(lo); + throw; + } + + _blocks.erase(_blocks.lower_bound(lo), _blocks.lower_bound(hi)); + _mark(first, count, false); + return true; + } + + bool _splitAt(uint64_t at) { + BlockMap::iterator it = _blocks.upper_bound(at); + if (it == _blocks.begin()) return false; + --it; + if (it->first == at || it->second.end <= at) return false; + + const Block tail = {it->second.end, it->second.type}; + _blocks.emplace(at, tail); + it->second.end = at; + return true; + } + + void _unsplitAt(uint64_t at) noexcept { + BlockMap::iterator right = _blocks.find(at); + BlockMap::iterator left = std::prev(right); + left->second.end = right->second.end; + _blocks.erase(right); + } std::mutex _mutex; - bool _used[NUM_PAGES] = {}; - std::map _blocks; + bool _used[kNumPages] = {}; + BlockMap _blocks; }; +} + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + try { + return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } } void DirectMemoryFree(int64_t start, size_t len) { - PhysicalMemoryPool::Instance().Free(static_cast(start), len); + (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { @@ -170,7 +283,7 @@ bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* en } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { - PhysicalMemoryPool::Instance().Retype(static_cast(start), len, 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) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 4540fa0c3b..e3912942b2 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -162,6 +162,10 @@ bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { return true; } +bool IsDeleted(const Snapshot& snapshot) { + return snapshot.deletePending || snapshot.links == 0; +} + std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { std::vector hints; if (!directory) { @@ -182,7 +186,7 @@ std::vector SnapshotHints(const Snapshot& before, const Snapshot& if ((after.deletePending && !before.deletePending) || after.links < before.links) { hints.push_back(NOTE_DELETE); } - if (!after.path.empty() && after.path != before.path) { + if (!after.path.empty() && after.path != before.path && !IsDeleted(after)) { hints.push_back(NOTE_RENAME); } return hints; @@ -276,7 +280,9 @@ void RunHostWatch(std::shared_ptr watch, HostWatch host) { } if (current.path != host.last.path) { CloseChange(host.parent); - host.parent = WatchParent(current.path); + if (!IsDeleted(current)) { + host.parent = WatchParent(current.path); + } } host.last = std::move(current); } diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index dc3b9e654e..4a14063e0e 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -27,6 +28,8 @@ bool DescriptorIsOpen_nid_no_patch(int descriptor); namespace File { int HostStandardError(); +[[nodiscard]] std::unique_lock LockPositionedTransfer(int descriptor); +void SharePositionedTransfer(int from, int to); } #endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index c52bdae4cd..3e7f3c0644 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -21,7 +21,7 @@ #ifdef _WIN32 #include #include -#include +#include "prx/libc/include/WindowsDescriptor.hpp" #else #include #include @@ -31,9 +31,6 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); -#ifdef _WIN32 -extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); -#endif namespace { @@ -89,6 +86,8 @@ constexpr std::int16_t FromStart = 0; constexpr std::int16_t FromCurrent = 1; constexpr std::int16_t FromEnd = 2; +constexpr std::uint32_t CloseOnExecRequest = 0x20006601; +constexpr std::uint32_t KeepOnExecRequest = 0x20006602; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; constexpr std::uint32_t RequestIn = 0x80000000u; @@ -137,20 +136,11 @@ const char* KindName(Kind kind) { } #ifdef _WIN32 -void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} - -HANDLE NativeHandle(int fd) { - const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); - const auto handle = ::_get_osfhandle(fd); - _set_thread_local_invalid_parameter_handler(previous); - return reinterpret_cast(handle); -} - std::optional Classify(int fd, const char* function) { if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; if (File::IsRandomDevice(fd)) return Kind::RandomDevice; - switch (::GetFileType(NativeHandle(fd))) { + switch (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd))) { case FILE_TYPE_DISK: return Kind::File; case FILE_TYPE_PIPE: return Kind::Pipe; case FILE_TYPE_CHAR: return Kind::Device; @@ -159,36 +149,25 @@ std::optional Classify(int fd, const char* function) { } int AccessMode(int fd, const char* function) { - using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); - static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); - PUBLIC_OBJECT_BASIC_INFORMATION information{}; - const NTSTATUS status = query == nullptr ? -1 : query(NativeHandle(fd), ObjectBasicInformation, &information, sizeof(information), nullptr); - if (status < 0) - throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(fd) + ", status=" + std::to_string(status)); - const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; - const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; - if (read && write) return ReadWrite; - if (write) return WriteOnly; - if (read) return ReadOnly; - throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " grants neither read nor write access"); + return WindowsDescriptorAccess_nid_no_patch(WindowsDescriptorHandle_nid_no_patch(fd), fd, function); } DWORD PipeState(int fd, const char* function) { DWORD state = 0; - if (!::GetNamedPipeHandleStateW(NativeHandle(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + if (!::GetNamedPipeHandleStateW(WindowsDescriptorHandle_nid_no_patch(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); return state; } void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); - if (!::SetNamedPipeHandleState(NativeHandle(fd), &mode, nullptr, nullptr)) + if (!::SetNamedPipeHandleState(WindowsDescriptorHandle_nid_no_patch(fd), &mode, nullptr, nullptr)) throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); } Entry InitialEntry(int fd) { int status = 0; - if (::GetFileType(NativeHandle(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + if (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; return {false, std::make_shared(status)}; } @@ -208,7 +187,7 @@ int Readable(int fd, Kind kind, const char* function) { } if (AccessMode(fd, function) == WriteOnly) return 0; DWORD available = 0; - if (!::PeekNamedPipe(NativeHandle(fd), nullptr, 0, nullptr, &available, nullptr)) { + if (!::PeekNamedPipe(WindowsDescriptorHandle_nid_no_patch(fd), nullptr, 0, nullptr, &available, nullptr)) { const DWORD error = ::GetLastError(); if (error == ERROR_BROKEN_PIPE) return 0; throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); @@ -532,6 +511,10 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); const auto kind = Classify(fd, "ioctl"); if (!kind) return Fail(GuestEbadf); + if (command == CloseOnExecRequest || command == KeepOnExecRequest) { + SetCloseOnExec(fd, command == CloseOnExecRequest); + return 0; + } if (command == NonblockingRequest) { const bool enable = *static_cast(argument) != 0; return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); @@ -565,6 +548,7 @@ void ShareDescriptor(int from, int to) { auto status = Materialize(from).status; g_entries[to] = Entry{false, std::move(status)}; #ifdef _WIN32 + File::SharePositionedTransfer(from, to); GuestFileLockShare_nid_no_patch(from, to); #endif } diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0868d227ca..4989903336 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -44,6 +44,7 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return descriptor; } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { + const auto positioned = File::LockPositionedTransfer(fd); const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); if (result != -2) return result; const auto position = ::_lseeki64(fd, offset, whence); @@ -55,14 +56,20 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } + const auto positioned = File::LockPositionedTransfer(fd); const int result = ::_read(fd, buf, static_cast(n)); if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; + if (result == 0 && File::DirectoryDescriptorPath(fd)) { + errno = EISDIR; + return -1; + } return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } + const auto positioned = File::LockPositionedTransfer(fd); const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); if (shared != -2) return static_cast(shared); const int result = ::_write(fd, buf, static_cast(n)); @@ -291,15 +298,13 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { if (path == nullptr) { throw std::invalid_argument(std::string(__func__) + ": path is null"); } - if (sb == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": sb is null"); - } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); native = File::PathOrRelinkedModule(native); std::error_code error; constexpr int GuestEnotdir = 20; + constexpr int GuestEfault = 14; const auto status = std::filesystem::status(native, error); if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (!std::filesystem::exists(status)) { @@ -307,6 +312,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); File::FillFileStat(native, sb); return 0; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index d9ceb8756b..58f00fafaa 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -26,9 +26,13 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include struct KernelIovec { @@ -105,7 +109,10 @@ static int NativeFchmod(int descriptor, int mode) { } static int NativeFtruncate(int descriptor, std::int64_t length) { const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); - return result == -2 ? static_cast(::_chsize_s(descriptor, length)) : result; + if (result != -2) return result; + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; + return static_cast(::_chsize_s(descriptor, length)); } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -169,6 +176,27 @@ static int NativeSync(int descriptor) { static int NativeFlock(int descriptor, int operation) { return File::Flock(descriptor, operation); } +struct PositionedLock { + std::mutex mutex; +}; +std::mutex g_positionedTableLock; +std::unordered_map> g_positionedLocks; +std::unique_lock File::LockPositionedTransfer(int descriptor) { + std::shared_ptr entry; + { + std::lock_guard table(g_positionedTableLock); + auto& slot = g_positionedLocks[descriptor]; + if (!slot) slot = std::make_shared(); + entry = slot; + } + return std::unique_lock(entry->mutex); +} +void File::SharePositionedTransfer(int from, int to) { + std::lock_guard table(g_positionedTableLock); + auto& source = g_positionedLocks[from]; + if (!source) source = std::make_shared(); + g_positionedLocks[to] = source; +} std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); @@ -182,9 +210,10 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ errno = EBADF; return -1; } + const auto positioned = File::LockPositionedTransfer(descriptor); LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { - errno = ESPIPE; + errno = !File::DirectoryDescriptorPath(descriptor) ? ESPIPE : write ? EBADF : EISDIR; return -1; } OVERLAPPED overlapped{}; @@ -208,6 +237,7 @@ static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); } + const auto positioned = File::LockPositionedTransfer(descriptor); if (write) { const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); if (result != -2) return result; @@ -218,6 +248,12 @@ static bool NativeIsDisk(int descriptor) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_DISK; } +static bool NativePipeHasData(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + DWORD available = 0; + return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_PIPE + && ::PeekNamedPipe(handle, nullptr, 0, nullptr, &available, nullptr) && available != 0; +} static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { return NativePositioned_nid_no_patch(descriptor, buf, nbytes, offset, false); } @@ -286,6 +322,7 @@ static const struct iovec* NativeIovecs(const KernelIovec* iov) { } #endif +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_ENOENT = 2; static constexpr int GUEST_EIO = 5; static constexpr int GUEST_EBADF = 9; @@ -297,12 +334,12 @@ static constexpr int GUEST_ENAMETOOLONG = 63; static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; +static constexpr int GUEST_EOPNOTSUPP = 45; static constexpr int GUEST_ENOTEMPTY = 66; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_ELOOP = 62; static constexpr int GUEST_EDQUOT = 69; static constexpr int GUEST_EOVERFLOW = 84; -static constexpr int GUEST_EOPNOTSUPP = 45; static int SceErrorFromErrno(int error); @@ -478,6 +515,33 @@ int File::DuplicateFrom(int fd, int start) { #endif } +static int LookupError(const std::filesystem::path& native) { + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (std::filesystem::exists(status)) return native.has_filename() || std::filesystem::is_directory(status) ? 0 : GUEST_ENOTDIR; + if (status.type() != std::filesystem::file_type::not_found) return SceErrorFromErrno(error.default_error_condition().value()) & 0xffff; + const bool fileWithSeparator = !native.has_filename() && std::filesystem::exists(WithoutTrailingSeparator(native), error); + return fileWithSeparator || HasNonDirectoryPrefix(native) ? GUEST_ENOTDIR : GUEST_ENOENT; +} + +static constexpr std::pair GUEST_FILE_FLAGS[] = { + {0x00000001, "UF_NODUMP"}, {0x00000002, "UF_IMMUTABLE"}, {0x00000004, "UF_APPEND"}, {0x00000008, "UF_OPAQUE"}, + {0x00000010, "UF_NOUNLINK"}, {0x00000080, "UF_SYSTEM"}, {0x00000100, "UF_SPARSE"}, {0x00000200, "UF_OFFLINE"}, + {0x00000400, "UF_REPARSE"}, {0x00000800, "UF_ARCHIVE"}, {0x00001000, "UF_READONLY"}, {0x00008000, "UF_HIDDEN"}, + {0x00010000, "SF_ARCHIVED"}, {0x00020000, "SF_IMMUTABLE"}, {0x00040000, "SF_APPEND"}, {0x00100000, "SF_NOUNLINK"}, + {0x00200000, "SF_SNAPSHOT"}}; +static constexpr std::uint64_t GUEST_SF_SNAPSHOT = 0x00200000; + +static std::string FileFlagNames(std::uint64_t flags) { + std::string names; + for (const auto& [flag, name] : GUEST_FILE_FLAGS) { + if ((flags & flag) == 0) continue; + if (!names.empty()) names += '|'; + names += name; + } + return names; +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -488,6 +552,18 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { 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"); + return 0; +} + int APS5_VABI close_nid_postfix(int d) { int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { @@ -645,7 +721,10 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (sb == nullptr) { APS5_INVALID_ARG_EX; } - if (GuestSockets::IsOpen(d)) throw std::runtime_error(std::string(__func__) + ": the status of a guest socket is not implemented"); + if (GuestSockets::IsOpen(d)) { + if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); + return 0; + } if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); return 0; } @@ -718,22 +797,15 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr) { - APS5_INVALID_ARG_EX; - } - if (offset < 0) { - APS5_INVALID_ARG_EX; - } + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; const GuestArena::HostWrite destination(buf, nbytes); - if (!destination.Open()) errno = EFAULT; - else if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); - auto n = destination.Open() ? NativePread(d, buf, nbytes, offset) : -1; - if (n < 0 && errno == EBADF) return PosixFailure(GUEST_EBADF); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pread failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + if (!destination.Open()) return PosixFailure(GUEST_EFAULT); + if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + char empty = 0; + const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { @@ -809,7 +881,10 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { int APS5_VABI sceKernelFstat(int d, FileStat* sb) { if (sb == nullptr) throw std::invalid_argument("sceKernelFstat: sb is null"); - if (GuestSockets::IsOpen(d)) throw std::runtime_error("sceKernelFstat: the status of a guest socket is not implemented"); + if (GuestSockets::IsOpen(d)) { + const int error = GuestSockets::Stat(d, sb); + return error == 0 ? 0 : SceErrorFromErrno(error); + } if (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); return 0; } @@ -1002,6 +1077,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write); return result < 0 ? SceErrorFromErrno(errno) : 0; } + if (!write && offset == nullptr && File::DirectoryDescriptorPath(d)) return SceErrorFromErrno(GUEST_EISDIR); const bool whole = write || offset != nullptr || NativeIsDisk(d); std::int64_t done = 0; for (int i = 0; i < iovcnt; ++i) { @@ -1012,7 +1088,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno); done += result; position += static_cast(result); - if (static_cast(result) < chunk || !whole) return done; + if (static_cast(result) < chunk || (!whole && !NativePipeHasData(d))) return done; } } return done; diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 1dc372c387..4dac33c1dd 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -24,6 +24,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); +#endif + namespace { thread_local std::array loaderError{}; thread_local bool pendingError = false; @@ -39,6 +43,7 @@ struct Module { if (owned && native) { #ifdef _WIN32 FreeLibrary(static_cast(native)); + ForgetUnloadedUnwindImages_nid_no_patch(); #else ::dlclose(native); #endif @@ -57,28 +62,23 @@ void* Symbol(Module& module, const char* name) { #endif } #ifndef _WIN32 -std::uintptr_t MainProgramBase() { - static const std::uintptr_t base = [] { - struct Scan { - std::uintptr_t value = 0; - }; - Scan scan; - ::dl_iterate_phdr( - [](dl_phdr_info* info, size_t, void* data) { - auto* found = static_cast(data); - if (found->value == 0 && info->dlpi_name != nullptr && info->dlpi_name[0] == '\0') found->value = info->dlpi_addr; - return found->value == 0 ? 0 : 1; - }, - &scan); - return scan.value; +const link_map* MainProgramMap() { + static const link_map* const map = [] { + void* handle = ::dlopen(nullptr, RTLD_LAZY); + link_map* found = nullptr; + const bool resolved = handle != nullptr && ::dlinfo(handle, RTLD_DI_LINKMAP, &found) == 0 && found != nullptr; + if (handle != nullptr) ::dlclose(handle); + if (!resolved) throw std::runtime_error("dlsym: cannot find the link map of the main program"); + return found; }(); - return base; + return map; } bool FromGuestModule(void* address) { Dl_info info{}; - if (address == nullptr || !::dladdr(address, &info) || info.dli_fname == nullptr) return false; - if (reinterpret_cast(info.dli_fbase) == MainProgramBase()) return true; + link_map* image = nullptr; + if (address == nullptr || !::dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || info.dli_fname == nullptr || image == nullptr) return false; + if (image == MainProgramMap()) return true; const std::string path(info.dli_fname); return path.ends_with(".prx"); } diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index d196d86131..6e06be8deb 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" @@ -11,6 +12,8 @@ #include "prx/libkernel/Module/EhFrame.hpp" #include "prx/libkernel/Module/ModuleArgs.hpp" #ifdef _WIN32 +#include +#include #include #include #elif defined(__APPLE__) @@ -24,19 +27,86 @@ #ifdef _WIN32 namespace { -void FillGuestUnwindInfo(const std::uint8_t* base, ModuleInfoForUnwind* info) { +struct UnwindImage { + DWORD size = 0; + bool hasTables = false; + EhFrame::Tables tables; + std::string name; +}; + +std::mutex unwindImagesMutex; +std::unordered_map& UnwindImages() { + static auto& images = *new std::unordered_map(); + return images; +} + +void WriteUnwindInfo(const UnwindImage& image, const std::uint8_t* base, ModuleInfoForUnwind* info) { + if (image.hasTables) { + info->eh_frame_hdr_addr = image.tables.header; + info->eh_frame_addr = image.tables.frames; + info->eh_frame_size = image.tables.framesSize; + info->seg0_addr = reinterpret_cast(base); + info->seg0_size = image.size; + } + std::strncpy(info->name, image.name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} + +void FillUnwindImage(void* allocationBase, ModuleInfoForUnwind* info) { + const auto* base = static_cast(allocationBase); + const auto key = reinterpret_cast(base); + { + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(key); + if (found != UnwindImages().end()) { + WriteUnwindInfo(found->second, base, info); + return; + } + } const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without a DOS header"); const auto* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error("sceKernelGetModuleInfoForUnwind: image without an NT header"); - EhFrame::Tables tables; - if (!EhFrame::ReadEhmeta(reinterpret_cast(base), *nt, "sceKernelGetModuleInfoForUnwind", tables)) return; - info->eh_frame_hdr_addr = tables.header; - info->eh_frame_addr = tables.frames; - info->eh_frame_size = tables.framesSize; - info->seg0_addr = reinterpret_cast(base); - info->seg0_size = nt->OptionalHeader.SizeOfImage; + UnwindImage fresh; + fresh.size = nt->OptionalHeader.SizeOfImage; + fresh.hasTables = EhFrame::ReadEhmeta(key, *nt, "sceKernelGetModuleInfoForUnwind", fresh.tables); + char path[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), allocationBase, path, sizeof(path) - 1); + fresh.name.assign(path, length); + std::lock_guard lock(unwindImagesMutex); + const auto& image = UnwindImages().insert_or_assign(key, std::move(fresh)).first->second; + WriteUnwindInfo(image, base, info); } + +bool FillKnownUnwindImage(std::uint64_t address, ModuleInfoForUnwind* info) { + PVOID allocationBase = nullptr; + if (RtlPcToFileHeader(reinterpret_cast(address), &allocationBase) == nullptr || allocationBase == nullptr) return false; + const auto* base = static_cast(allocationBase); + std::lock_guard lock(unwindImagesMutex); + const auto found = UnwindImages().find(reinterpret_cast(base)); + if (found == UnwindImages().end() || !found->second.hasTables) return false; + info->st_size = sizeof(ModuleInfoForUnwind); + WriteUnwindInfo(found->second, base, info); + return true; +} +} + +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch() { + std::vector bases; + { + std::lock_guard lock(unwindImagesMutex); + for (const auto& image : UnwindImages()) bases.push_back(image.first); + } + std::vector unloaded; + for (const auto base : bases) { + HMODULE loaded = nullptr; + const bool present = GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(base), &loaded) != 0 && + reinterpret_cast(loaded) == base; + if (!present) unloaded.push_back(base); + } + if (unloaded.empty()) return; + std::lock_guard lock(unwindImagesMutex); + for (const auto base : unloaded) UnwindImages().erase(base); } #elif !defined(__APPLE__) extern "C" int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info); @@ -105,6 +175,7 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn (void)flags; if (!info) return SCE_KERNEL_ERROR_EFAULT; #ifdef _WIN32 + if (FillKnownUnwindImage(addr, info)) return 0; MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) return SCE_KERNEL_ERROR_ESRCH; info->st_size = sizeof(ModuleInfoForUnwind); @@ -113,9 +184,12 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn info->eh_frame_size = 0; info->seg0_addr = reinterpret_cast(mbi.BaseAddress); info->seg0_size = mbi.RegionSize; - if (mbi.Type == MEM_IMAGE) FillGuestUnwindInfo(static_cast(mbi.AllocationBase), info); + if (mbi.Type == MEM_IMAGE) { + FillUnwindImage(mbi.AllocationBase, info); + return 0; + } char path[4096] = {}; - DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); + const DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); path[len] = '\0'; std::strncpy(info->name, path, sizeof(info->name) - 1); info->name[sizeof(info->name) - 1] = '\0'; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp new file mode 100644 index 0000000000..2becc20064 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp @@ -0,0 +1,24 @@ +#include "../include/Atfork.hpp" +#include +#include + +namespace { +constexpr int GuestNoMemory = 12; +std::mutex atforkLock; +std::vector atforkHandlers; +} + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + std::lock_guard lock(atforkLock); + try { + atforkHandlers.push_back({prepare, parent, child}); + } catch (const std::bad_alloc&) { + return GuestNoMemory; + } + return 0; +} + +std::vector PthreadAtforkHandlers_nid_no_patch() { + std::lock_guard lock(atforkLock); + return atforkHandlers; +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 38eef15ffe..90ad038f81 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -163,6 +163,21 @@ int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpu return 0; } +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset) { + constexpr int GuestEdeadlk = 11; + if (thread != scePthreadSelf()) { + if (!thread) return PosixThread::GUEST_EINVAL; + if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + } + if (cpusetSize != GUEST_CPUSET_BYTES) return GUEST_ERANGE; + if (!cpuset) return PosixThread::GUEST_EFAULT; + KernelCpumask requested = 0; + std::memcpy(&requested, cpuset, sizeof(requested)); + const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); + if (affinity == 0) return GuestEdeadlk; + return PosixThread::ToErrno(scePthreadSetaffinity(thread, affinity)); +} + int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } diff --git a/core/libs/prx/libkernel/Pthread/include/Atfork.hpp b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp new file mode 100644 index 0000000000..fa50990a6e --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/include/Atfork.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_PTHREAD_ATFORK_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +using PthreadAtforkHandler = void (APS5_VABI*)(); + +struct PthreadAtforkHandlers { + PthreadAtforkHandler prepare; + PthreadAtforkHandler parent; + PthreadAtforkHandler child; +}; + +extern "C" int APS5_VABI pthread_atfork_nid_postfix(PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child); + +std::vector PthreadAtforkHandlers_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 4cddf4d5b8..d576c64ff0 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -60,6 +60,19 @@ int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { return 0; } +template +struct WakingLock { + PthreadMutex mutex; + TNative& native; + void lock() { native.lock(); } + void unlock() { + native.unlock(); +#ifdef _WIN32 + TimedWait::Unpark(mutex); +#endif + } +}; + int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { auto* c = resolveCond(cond); PthreadMutex m = nullptr; @@ -72,7 +85,8 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()), timedOut); } } trace{caller, timedOut, waitStart}; if (m->_type == MutexType::Recursive) { - std::unique_lock lock(m->_rmtx, std::adopt_lock); + WakingLock waking{m, m->_rmtx}; + std::unique_lock lock(waking, std::adopt_lock); const auto previousCount = m->_count; m->_count = 0; m->_owner.store(std::thread::id{}, std::memory_order_release); @@ -87,7 +101,8 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional lock(m->_mtx, std::adopt_lock); + WakingLock waking{m, m->_mtx}; + std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 4cf22db6ef..93e0e693b6 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -2,6 +2,7 @@ #include "../include/Mutex.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -87,14 +88,18 @@ int MutexOperations::Timedlock(PthreadMutex* mutex, const KernelTimespec* abstim if (std::chrono::duration(duration) >= std::chrono::duration(std::chrono::system_clock::duration::max())) throw std::overflow_error("Absolute mutex timeout exceeds the host clock range"); const auto deadline = std::chrono::system_clock::time_point(std::chrono::duration_cast(duration)); - return acquireMutex(resolveMutex(mutex, true), [&](auto& native) { + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [&](auto& native) { #ifdef _WIN32 - while (!native.try_lock()) { - if (std::chrono::system_clock::now() >= deadline) return false; - TimedWait::SleepNanos(1000000); + for (;;) { + const auto now = std::chrono::system_clock::now(); + if (now >= deadline) return native.try_lock(); + const auto remaining = std::chrono::duration_cast(deadline - now).count(); + const std::uint64_t slice = TimedWait::NowNanos() + static_cast(std::min(remaining, 100000000)); + if (TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, &slice)) return true; } - return true; #else + static_cast(current); return native.try_lock_until(deadline); #endif }, sceTimedOut, false); @@ -169,13 +174,15 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { } int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex) { - return acquireMutex(resolveMutex(mutex, true), [](auto& native) { + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [current](auto& native) { #ifdef _WIN32 - while (!native.try_lock()) TimedWait::SleepNanos(1000000); + return TimedWait::ParkAcquire(current, [&] { return native.try_lock(); }, nullptr); #else + static_cast(current); native.lock(); -#endif return true; +#endif }, 0, false); } @@ -183,21 +190,29 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex) { auto* current = resolveMutex(mutex, false); if (current->_owner.load(std::memory_order_acquire) != std::this_thread::get_id()) return sceNotPermitted; + bool released = true; if (current->_type == MutexType::Recursive) { - if (--current->_count == 0) + released = --current->_count == 0; + if (released) current->_owner.store(std::thread::id{}, std::memory_order_release); current->_rmtx.unlock(); } else { current->_owner.store(std::thread::id{}, std::memory_order_release); current->_mtx.unlock(); } +#ifdef _WIN32 + if (released) TimedWait::Unpark(current); +#else + static_cast(released); +#endif return 0; } int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) { const auto deadline = TimedWait::DeadlineNanos(usec); - return acquireMutex(resolveMutex(mutex, true), [=](auto& native) { - return TimedWait::AcquireUntil(deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); + auto* current = resolveMutex(mutex, true); + return acquireMutex(current, [=](auto& native) { + return TimedWait::AcquireUntil(current, deadline, [&] { return native.try_lock(); }, [&](std::uint64_t micros) { return native.try_lock_for(std::chrono::microseconds(micros)); }); }, sceTimedOut, false); } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index cdbf5c9ab7..caeb5c291d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -125,8 +125,8 @@ static void AllocateGuestStack(PthreadPrivate* thread, std::size_t usableBytes) static void FreeGuestStack(PthreadPrivate* thread) { if (thread->guestStack == nullptr) return; void* const mapping = thread->guestStack; - DoMunmap(thread->guestStack, thread->guestStackBytes); UnregisterGuestStackMapping(mapping); + DoMunmap(thread->guestStack, thread->guestStackBytes); thread->guestStack = nullptr; thread->guestStackBytes = 0; } @@ -152,8 +152,8 @@ static void ReapRetiredStacks() { } } for (const auto& stack : ready) { - DoMunmap(stack.address, stack.bytes); UnregisterGuestStackMapping(stack.address); + DoMunmap(stack.address, stack.bytes); } } diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp new file mode 100644 index 0000000000..d41d47b63c --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +namespace GuestSockets { + +struct SocketOptionState { + int reportedError = 0; +}; + +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); + +} diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 103f2d0684..a39295ecd6 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -3,6 +3,8 @@ #include #include #include "prx/libkernel/Socket/include/SocketPoll.hpp" + +struct FileStat; namespace GuestSockets { struct Iovec { void* base; @@ -16,8 +18,12 @@ bool IsOpen(int descriptor); int Family(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); bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof); int Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds); +#ifdef _WIN32 +bool PipeReady(int descriptor, short events, short& revents); +#endif struct Interest { int descriptor; bool write; diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 5082e538ee..e01a8b7f34 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -2,6 +2,8 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" @@ -11,7 +13,6 @@ #include #include #include -#include #include #include @@ -21,6 +22,7 @@ namespace { +constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; constexpr int SelectDescriptorLimit = 1024; constexpr int SelectPollSliceMilliseconds = 10; @@ -38,8 +40,8 @@ bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } -void UnknownDescriptor(int descriptor, bool polled) { - const auto what = "select on descriptor " + std::to_string(descriptor) + (polled ? " (not a libSceNet socket)" : " (libSceNet is not loaded)"); +void UnknownDescriptor(int descriptor) { + const auto what = "select on descriptor " + std::to_string(descriptor) + " (not a libSceNet socket)"; NotImplemented_nid_no_patch(what.c_str()); } @@ -91,6 +93,25 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise return result; } +#else + +int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMilliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMilliseconds); + for (;;) { + int ready = 0; + for (int index = 0; index < count; ++index) { + auto& entry = descriptors[index]; + if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) return -GuestEbadf; + if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) { + NotImplemented_nid_no_patch(("select on descriptor " + std::to_string(entry.descriptor) + " (neither a socket nor a pipe)").c_str()); + } + if (entry.revents != 0) ++ready; + } + if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + #endif } @@ -165,10 +186,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* std::vector groups; if (!kernel.entries.empty()) groups.push_back(std::move(kernel)); if (!net.entries.empty()) { - PollGroup hosts{nullptr, {}, {}}; -#ifndef _WIN32 - hosts.poll = &HostPoll; -#endif + PollGroup hosts{&HostPoll, {}, {}}; net.poll = g_socketPoller.load(); if (net.poll != nullptr) { for (auto& socket : net.entries) socket.revents = 0; @@ -184,7 +202,6 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* continue; } const int descriptor = net.entries[index].descriptor; - if (hosts.poll == nullptr) UnknownDescriptor(descriptor, net.poll != nullptr); hosts.entries.push_back({descriptor, net.entries[index].events, 0}); hosts.slots.push_back(net.slots[index]); net.entries.erase(net.entries.begin() + static_cast(index)); @@ -205,7 +222,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* } if (devicesReady) waitMilliseconds = 0; if (groups.empty()) { - if (waitMilliseconds > 0) std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); + if (waitMilliseconds > 0) PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds))); } else { const int slice = groups.size() == 1 ? waitMilliseconds : std::min(waitMilliseconds, SelectPollSliceMilliseconds); const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); @@ -226,7 +243,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* } int ready = 0; for (const auto& entry : requested) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor, true); + if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor); const bool readable = (entry.events & KernelSocketPoll::Readable) != 0 && (entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) != 0; const bool writable = (entry.events & KernelSocketPoll::Writable) != 0 && (entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) != 0; const bool urgent = (entry.events & KernelSocketPoll::Urgent) != 0 && (entry.revents & KernelSocketPoll::Urgent) != 0; diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp new file mode 100644 index 0000000000..747bf83a97 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -0,0 +1,150 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#endif +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" +#include +#include + +namespace GuestSockets { +namespace { + +constexpr int GuestAcceptConnection = 0x2; +constexpr int GuestReuseAddress = 0x4; +constexpr int GuestKeepAlive = 0x8; +constexpr int GuestDontRoute = 0x10; +constexpr int GuestBroadcast = 0x20; +constexpr int GuestLinger = 0x80; +constexpr int GuestOobInline = 0x100; +constexpr int GuestSendBuffer = 0x1001; +constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestError = 0x1007; +constexpr int GuestType = 0x1008; + +constexpr int ErrorInvalid = 22; +constexpr int ErrorDomain = 33; +constexpr int ErrorNoProtocolOption = 42; +constexpr int MaximumLinger = 65535; + +struct GuestLingerValue { + std::int32_t enabled; + std::int32_t seconds; +}; + +#ifdef _WIN32 +using NativeHandle = SOCKET; +using NativeOptionValue = const char*; +using NativeOptionOutput = char*; +int HostError() { return GuestErrorFromHost(WSAGetLastError()); } +#else +using NativeHandle = int; +using NativeOptionValue = const void*; +using NativeOptionOutput = void*; +int HostError() { return GuestErrorFromHost(errno); } +#endif + +int NativeBoolean(int option) { + switch (option) { + case GuestReuseAddress: return SO_REUSEADDR; + case GuestKeepAlive: return SO_KEEPALIVE; + case GuestDontRoute: return SO_DONTROUTE; + case GuestBroadcast: return SO_BROADCAST; + case GuestOobInline: return SO_OOBINLINE; + default: return -1; + } +} + +int Copy(void* output, std::uint32_t* length, const void* value, std::uint32_t size) { + const std::uint32_t count = std::min(*length, size); + if (count != 0 && !output) return ErrorInvalid; + if (count != 0) std::memcpy(output, value, count); + *length = count; + return 0; +} + +} + +int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length) { + const auto handle = static_cast(native); + if (option == GuestLinger) { + if (length < sizeof(GuestLingerValue)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestLingerValue guest; + std::memcpy(&guest, value, sizeof(guest)); + if (guest.seconds < 0 || guest.seconds > MaximumLinger) return ErrorDomain; +#ifdef _WIN32 + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = static_cast(guest.seconds); +#else + linger request{}; + request.l_onoff = guest.enabled != 0 ? 1 : 0; + request.l_linger = guest.seconds; +#endif + if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); + return 0; + } + const bool native_boolean = NativeBoolean(option) != -1; + if (native_boolean || option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (length < sizeof(int)) return ErrorInvalid; + if (!value) return ErrorInvalid; + int number; + std::memcpy(&number, value, sizeof(number)); + if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + if (number < 1) return ErrorInvalid; + const int nativeOption = option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF; + if (::setsockopt(handle, SOL_SOCKET, nativeOption, reinterpret_cast(&number), sizeof(number))) return HostError(); + return 0; + } + const int enabled = number != 0 ? 1 : 0; + if (::setsockopt(handle, SOL_SOCKET, NativeBoolean(option), reinterpret_cast(&enabled), sizeof(enabled))) return HostError(); + return 0; + } + return ErrorNoProtocolOption; +} + +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) { + linger current{}; + socklen_t size = sizeof(current); + if (::getsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(¤t), &size)) return HostError(); + const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; + return Copy(value, length, &guest, sizeof(guest)); + } + int number = 0; + if (const int nativeOption = NativeBoolean(option); nativeOption != -1) { + int enabled = 0; + socklen_t size = sizeof(enabled); + 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; + } else if (option == GuestType) { + number = type; + } else if (option == GuestError) { + int pending = 0; + socklen_t size = sizeof(pending); + if (::getsockopt(handle, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size)) return HostError(); +#ifdef _WIN32 + if (pending != 0 && pending == state.reportedError) pending = 0; + else if (pending != 0) state.reportedError = pending; +#endif + number = pending == 0 ? 0 : GuestErrorFromHost(pending); + } else if (option == GuestSendBuffer || option == GuestReceiveBuffer) { + socklen_t size = sizeof(number); + if (::getsockopt(handle, SOL_SOCKET, option == GuestSendBuffer ? SO_SNDBUF : SO_RCVBUF, reinterpret_cast(&number), &size)) return HostError(); + } else { + return ErrorNoProtocolOption; + } + return Copy(value, length, &number, sizeof(number)); +} + +} diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7a0304ce10..e33cc71239 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -15,10 +15,13 @@ #include #include #endif +#include "SceTypes.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" +#include "prx/libkernel/Socket/include/SocketOptions.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -67,6 +70,12 @@ int NativeError() { return GuestSockets::GuestErrorFromHost(errno); #endif } +int ConnectError() { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) return 36; +#endif + return NativeError(); +} int SendError(int type) { #ifdef _WIN32 if (type == 2 && WSAGetLastError() == WSAENOTCONN) return 39; @@ -81,6 +90,9 @@ struct Socket { int type; std::mutex modeMutex; bool nonblocking = false; + GuestSockets::SocketOptionState options; + bool receiveShut = false; + bool sendShut = false; Socket(NativeSocket value, int family, int type) : value(value), family(family), type(type) {} ~Socket() { if (value == Invalid) return; @@ -187,15 +199,6 @@ std::shared_ptr Lookup(int descriptor) { Fail(9); return {}; } -int Option(int guest) { - switch (guest) { - case 0x4: return SO_REUSEADDR; - case 0x20: return SO_BROADCAST; - case 0x1001: return SO_SNDBUF; - case 0x1002: return SO_RCVBUF; - default: return -1; - } -} bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, socklen_t& size) { if (!input || length < 2) { Fail(14); return false; } const auto* bytes = static_cast(input); @@ -471,6 +474,12 @@ bool IsPipe(int descriptor, short events) { #endif } +#ifdef _WIN32 +bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { + return PipeReadiness(descriptor, events, revents); +} +#endif + int GuestSockets::Duplicate(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); @@ -596,6 +605,41 @@ int UnnamedAddress(void* address, std::uint32_t* length) { } } +int GuestSockets::Stat(int descriptor, FileStat* status) { + std::shared_ptr socket; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return 9; + socket = found->second; + } + bool receiveShut = false; + bool sendShut = false; + { + std::lock_guard lock(socket->modeMutex); + receiveShut = socket->receiveShut; + sendShut = socket->sendShut; + } + std::int64_t available = 0; +#ifdef _WIN32 + unsigned long count = 0; + if (ioctlsocket(socket->value, FIONREAD, &count) == 0) available = static_cast(count); +#else + int count = 0; + if (::ioctl(socket->value, FIONREAD, &count) == 0) available = count; +#endif + int sendBuffer = 0; + socklen_t length = sizeof(sendBuffer); + if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&sendBuffer), &length) != 0) sendBuffer = 0; + *status = FileStat{}; + status->st_mode = 0140000; + if (!receiveShut || available > 0) status->st_mode |= 0444; + if (!sendShut) status->st_mode |= 0222; + status->st_size = available; + status->st_blksize = sendBuffer > 0 ? static_cast(sendBuffer) : 0; + return 0; +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr socket; { @@ -732,26 +776,20 @@ int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, } const auto socket = Lookup(descriptor); if (!socket) return -1; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || length < sizeof(int)) return Fail(22); - return ::setsockopt(socket->value, SOL_SOCKET, nativeOption, - static_cast(value), sizeof(int)) ? Fail(NativeError()) : 0; + if (level != 0xffff) return Fail(42); + std::lock_guard lock(socket->modeMutex); + const int error = GuestSockets::SetSocketLevelOption(static_cast(socket->value), socket->options, option, value, length); + return error == 0 ? 0 : Fail(error); } int APS5_VABI getsockopt_nid_postfix(int descriptor, int level, int option, void* value, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; - const int nativeOption = Option(option); - if (level != 0xffff || nativeOption == -1) return Fail(42); - if (!value || !length || *length < sizeof(int)) return Fail(22); - int result = 0; - socklen_t size = sizeof(result); - if (::getsockopt(socket->value, SOL_SOCKET, nativeOption, reinterpret_cast(&result), &size)) - return Fail(NativeError()); - std::memcpy(value, &result, sizeof(result)); - *length = sizeof(result); - return 0; + if (level != 0xffff) return Fail(42); + if (!length) return Fail(22); + std::lock_guard lock(socket->modeMutex); + const int error = GuestSockets::GetSocketLevelOption(static_cast(socket->value), socket->options, socket->type, option, value, length); + return error == 0 ? 0 : Fail(error); } int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { const bool nonblocking = (type & GuestNonBlocking) != 0; @@ -828,7 +866,11 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint socklen_t size; if (!Address(address, length, native, size)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); - return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; + { + std::lock_guard lock(socket->modeMutex); + socket->options.reportedError = 0; + } + return ::connect(socket->value, reinterpret_cast(&native), size) ? Fail(ConnectError()) : 0; } int APS5_VABI listen_nid_postfix(int descriptor, int backlog) { const auto socket = Lookup(descriptor); @@ -899,7 +941,11 @@ int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { const auto socket = Lookup(descriptor); if (!socket) return Fail(9); if (how < 0 || how > 2) return Fail(22); - return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; + if (::shutdown(socket->value, how)) return Fail(NativeError()); + std::lock_guard lock(socket->modeMutex); + if (how != 1) socket->receiveShut = true; + if (how != 0) socket->sendShut = true; + return 0; } int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); @@ -1228,7 +1274,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (native.empty() && pipes.empty()) { if (ready || timeout == 0) return ready; if (timeout < 0) throw std::runtime_error("poll: an infinite wait without sockets or pipes never returns"); - std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); + PreciseSleepUs(static_cast(timeout) * 1000); return 0; } const auto collect = [&](std::size_t i, bool pipe) { diff --git a/core/libs/prx/libkernel/System/include/HostCpu.hpp b/core/libs/prx/libkernel/System/include/HostCpu.hpp new file mode 100644 index 0000000000..11b3368dbf --- /dev/null +++ b/core/libs/prx/libkernel/System/include/HostCpu.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_HOSTCPU_HPP + +#include +#include +#include + +struct HostCpuRegisters { + std::uint32_t Leaf1Ecx = 0; + std::uint32_t Leaf1Edx = 0; + std::uint32_t Leaf7Ebx = 0; + std::uint32_t Leaf7Ecx = 0; + std::uint32_t LeafDSub1Eax = 0; + std::uint32_t Leaf80000001Ecx = 0; + std::uint32_t Leaf80000008Ebx = 0; + std::uint64_t Xcr0 = 0; +}; + +struct HostCpuFeature { + const char* Name; + bool Required; + bool Present; +}; + +HostCpuRegisters ReadHostCpuRegisters(); +std::string HostCpuModel(); +std::vector HostCpuFeatures(const HostCpuRegisters& registers); +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers); +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model); + +#endif diff --git a/core/libs/prx/libkernel/System/src/HostCpu.cpp b/core/libs/prx/libkernel/System/src/HostCpu.cpp new file mode 100644 index 0000000000..a27d29b6dc --- /dev/null +++ b/core/libs/prx/libkernel/System/src/HostCpu.cpp @@ -0,0 +1,199 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include "prx/libc/include/general/LogMacros.hpp" +#if defined(__x86_64__) || defined(__i386__) +#include +#endif +#include +#include +#include +#include +#include +#include + +namespace { + +enum class Register : std::uint8_t { + Leaf1Ecx, + Leaf1Edx, + Leaf7Ebx, + Leaf7Ecx, + LeafDSub1Eax, + Leaf80000001Ecx, + Leaf80000008Ebx +}; + +struct FeatureBit { + const char* Name; + Register Source; + unsigned Bit; + bool Required; +}; + +constexpr FeatureBit kFeatures[] = { + {"SSE", Register::Leaf1Edx, 25, true}, + {"SSE2", Register::Leaf1Edx, 26, true}, + {"SSE3", Register::Leaf1Ecx, 0, true}, + {"SSSE3", Register::Leaf1Ecx, 9, true}, + {"SSE4.1", Register::Leaf1Ecx, 19, true}, + {"SSE4.2", Register::Leaf1Ecx, 20, true}, + {"POPCNT", Register::Leaf1Ecx, 23, true}, + {"LZCNT", Register::Leaf80000001Ecx, 5, true}, + {"BMI1", Register::Leaf7Ebx, 3, true}, + {"BMI2", Register::Leaf7Ebx, 8, true}, + {"MOVBE", Register::Leaf1Ecx, 22, true}, + {"CMPXCHG16B", Register::Leaf1Ecx, 13, true}, + {"LAHF/SAHF", Register::Leaf80000001Ecx, 0, true}, + {"AVX", Register::Leaf1Ecx, 28, true}, + {"AVX2", Register::Leaf7Ebx, 5, true}, + {"FMA", Register::Leaf1Ecx, 12, true}, + {"F16C", Register::Leaf1Ecx, 29, true}, + {"AES", Register::Leaf1Ecx, 25, false}, + {"PCLMULQDQ", Register::Leaf1Ecx, 1, false}, + {"RDRAND", Register::Leaf1Ecx, 30, false}, + {"RDSEED", Register::Leaf7Ebx, 18, false}, + {"ADX", Register::Leaf7Ebx, 19, false}, + {"SHA", Register::Leaf7Ebx, 29, false}, + {"CLFLUSHOPT", Register::Leaf7Ebx, 23, false}, + {"CLWB", Register::Leaf7Ebx, 24, false}, + {"RDPID", Register::Leaf7Ecx, 22, false}, + {"XSAVEOPT", Register::LeafDSub1Eax, 0, false}, + {"XSAVEC", Register::LeafDSub1Eax, 1, false}, + {"SSE4A", Register::Leaf80000001Ecx, 6, false}, + {"PREFETCHW", Register::Leaf80000001Ecx, 8, false}, + {"CLZERO", Register::Leaf80000008Ebx, 0, false}, +}; + +constexpr unsigned kOsxsaveBit = 27; +constexpr std::uint64_t kXcr0SseAvxState = 0x6; + +std::uint32_t Value(const HostCpuRegisters& registers, const Register source) { + switch (source) { + case Register::Leaf1Ecx: + return registers.Leaf1Ecx; + case Register::Leaf1Edx: + return registers.Leaf1Edx; + case Register::Leaf7Ebx: + return registers.Leaf7Ebx; + case Register::Leaf7Ecx: + return registers.Leaf7Ecx; + case Register::LeafDSub1Eax: + return registers.LeafDSub1Eax; + case Register::Leaf80000001Ecx: + return registers.Leaf80000001Ecx; + case Register::Leaf80000008Ebx: + return registers.Leaf80000008Ebx; + } + throw std::logic_error("unknown CPUID register"); +} + +bool AvxStateEnabled(const HostCpuRegisters& registers) { + return (registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0 && (registers.Xcr0 & kXcr0SseAvxState) == kXcr0SseAvxState; +} + +std::string DisplayModel(const std::string& model) { + const auto end = model.find_last_not_of(' '); + return end == std::string::npos ? std::string("unknown model") : model.substr(0, end + 1); +} + +std::string Join(const std::vector& names) { + std::string text; + for (const auto& name : names) { + if (!text.empty()) text += ", "; + text += name; + } + return text; +} + +} + +HostCpuRegisters ReadHostCpuRegisters() { + HostCpuRegisters registers; +#if defined(__x86_64__) || defined(__i386__) + unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; + const unsigned int basic = __get_cpuid_max(0, nullptr); + if (basic >= 1) { + __cpuid(1, eax, ebx, ecx, edx); + registers.Leaf1Ecx = ecx; + registers.Leaf1Edx = edx; + } + if (basic >= 7) { + __cpuid_count(7, 0, eax, ebx, ecx, edx); + registers.Leaf7Ebx = ebx; + registers.Leaf7Ecx = ecx; + } + if (basic >= 0xD) { + __cpuid_count(0xD, 1, eax, ebx, ecx, edx); + registers.LeafDSub1Eax = eax; + } + const unsigned int extended = __get_cpuid_max(0x80000000, nullptr); + if (extended >= 0x80000001) { + __cpuid(0x80000001, eax, ebx, ecx, edx); + registers.Leaf80000001Ecx = ecx; + } + if (extended >= 0x80000008) { + __cpuid(0x80000008, eax, ebx, ecx, edx); + registers.Leaf80000008Ebx = ebx; + } + if ((registers.Leaf1Ecx >> kOsxsaveBit & 1) != 0) { + std::uint32_t low = 0, high = 0; + __asm__ volatile("xgetbv" : "=a"(low), "=d"(high) : "c"(0)); + registers.Xcr0 = static_cast(high) << 32 | low; + } +#endif + return registers; +} + +std::string HostCpuModel() { +#if defined(__x86_64__) || defined(__i386__) + if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; + unsigned int brand[12] = {}; + for (unsigned int leaf = 0; leaf < 3; ++leaf) + __get_cpuid(0x80000002 + leaf, &brand[leaf * 4], &brand[leaf * 4 + 1], &brand[leaf * 4 + 2], &brand[leaf * 4 + 3]); + std::string model(reinterpret_cast(brand), strnlen(reinterpret_cast(brand), sizeof(brand))); + model.erase(0, model.find_first_not_of(' ')); + return model; +#else + return {}; +#endif +} + +std::vector HostCpuFeatures(const HostCpuRegisters& registers) { + std::vector features; + features.reserve(std::size(kFeatures) + 1); + for (const auto& feature : kFeatures) + features.push_back({feature.Name, feature.Required, (Value(registers, feature.Source) >> feature.Bit & 1) != 0}); + features.push_back({"OS AVX state (XCR0)", true, AvxStateEnabled(registers)}); + return features; +} + +std::vector MissingHostCpuFeatures(const HostCpuRegisters& registers) { + std::vector missing; + for (const auto& feature : HostCpuFeatures(registers)) + if (feature.Required && !feature.Present) missing.emplace_back(feature.Name); + return missing; +} + +void CheckHostCpu(const HostCpuRegisters& registers, const std::string& model) { + const auto missing = MissingHostCpuFeatures(registers); + if (missing.empty()) return; + throw std::runtime_error("The host CPU (" + DisplayModel(model) + ") lacks " + Join(missing) + + ". The PS5's Zen 2 CPU has these extensions and titles run their instructions natively, so this CPU cannot run them."); +} + +#if (defined(__x86_64__) || defined(__i386__)) && !defined(__APPLE__) +namespace { + +const bool HostCpuChecked = [] { + const auto registers = ReadHostCpuRegisters(); + const auto model = HostCpuModel(); + std::vector present; + std::vector absent; + for (const auto& feature : HostCpuFeatures(registers)) + (feature.Present ? present : absent).emplace_back(feature.Name); + APS5_LOG_OUT("host CPU %s: has %s; lacks %s", DisplayModel(model).c_str(), Join(present).c_str(), absent.empty() ? "none" : Join(absent).c_str()); + CheckHostCpu(registers, model); + return true; +}(); + +} +#endif diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index b3d5ed436b..2d13a53816 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -118,10 +118,15 @@ bool hasPriorityRange(int policy) { return policy == 1 || policy == 3; } +constexpr int guestNoProcess = 3; constexpr int guestFault = 14; constexpr int guestInvalid = 22; constexpr int guestRlimitCpu = 0; constexpr int guestRlimitFsize = 1; +constexpr int guestPriorityProcess = 0; +constexpr int guestPriorityGroup = 1; +constexpr int guestPriorityUser = 2; +constexpr int guestNice = 0; constexpr int guestRlimitData = 2; constexpr int guestRlimitCore = 4; constexpr int guestRlimitSbsize = 9; @@ -548,6 +553,25 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { } } +int APS5_VABI getpriority_nid_postfix(int which, int who) { + if (which != guestPriorityProcess && which != guestPriorityGroup && which != guestPriorityUser) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + const bool found = which == guestPriorityUser ? (who == 0 ? getuid_nid_postfix() : who) == getuid_nid_postfix() + : who == 0 || who == getpid_nid_postfix(); + if (!found) { + *__error_nid_postfix() = guestNoProcess; + return -1; + } + return guestNice; +} + +int APS5_VABI getdtablesize_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return -1; +} + int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index bcf6eff7f7..90472da544 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -19,6 +19,10 @@ #include #endif +#ifdef _WIN32 +extern "C" void ForgetUnloadedUnwindImages_nid_no_patch(); +#endif + namespace { std::mutex rtldHeapMutex; @@ -69,7 +73,9 @@ int releaseThreadAtexitImage(const void* address, const char* caller) { } if (!handle) return 0; #ifdef _WIN32 - if (!FreeLibrary(static_cast(handle))) + const bool released = FreeLibrary(static_cast(handle)); + ForgetUnloadedUnwindImages_nid_no_patch(); + if (!released) #else if (dlclose(handle) != 0) #endif diff --git a/core/libs/prx/libkernel/System/src/SignalSets.cpp b/core/libs/prx/libkernel/System/src/SignalSets.cpp index 871866cb31..688ad49ce8 100644 --- a/core/libs/prx/libkernel/System/src/SignalSets.cpp +++ b/core/libs/prx/libkernel/System/src/SignalSets.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -60,11 +61,10 @@ int APS5_VABI sigismember_nid_postfix(const GuestSignalSet* set, int signal) { return (set->bits[WordIndex(signal)] & Bit(signal)) != 0 ? 1 : 0; } } -#ifdef _WIN32 -#define APS5_SIGNAL_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" -#define APS5_SIGNAL_ASM_CALL(name) " call " name "\n" +#define APS5_SIGNAL_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define APS5_SIGNAL_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" #else -#define APS5_SIGNAL_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" #define APS5_SIGNAL_ASM_CALL(name) " call " name "@PLT\n" #endif @@ -109,6 +109,16 @@ asm(".text\n" " popq %rsi\n" " popq %rdi\n" "2:\n" +#ifdef _WIN32 + " pushq %rdi\n" + " pushq %rsi\n" + " movq 16(%rdi), %rdi\n" + " subq $8, %rsp\n" + " call GuestStackSwitch_nid_no_patch\n" + " addq $8, %rsp\n" + " popq %rsi\n" + " popq %rdi\n" +#endif " movq %rdi, %rdx\n" " movl %esi, %eax\n" " movq 0(%rdx), %rcx\n" diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 01226d305d..7967f91297 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -559,9 +559,10 @@ int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { } int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { - if (set == nullptr || received == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + if (set == nullptr || received == nullptr || !SetHasSignal(*set)) return 22; int guest = 0; - if (WaitForPendingInSet(*set, true, nullptr, &guest) != 0) { *__error_nid_postfix() = GuestEagain; return -1; } + const int result = WaitForPendingInSet(*set, true, nullptr, &guest); + if (result != 0) return result; *received = guest; return 0; } @@ -583,6 +584,17 @@ int APS5_VABI pause_nid_postfix(void) { } } } + +int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { + if (set == nullptr) { *__error_nid_postfix() = 14; return -1; } + auto& pending = Pending(); + std::lock_guard lock(pending.lock); + set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); + set->bits[1] = 0; + set->bits[2] = 0; + set->bits[3] = 0; + return 0; +} } extern "C" { diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 672c9d2911..ba1525bda9 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -1,10 +1,8 @@ #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 -#if defined(__x86_64__) || defined(__i386__) -#include -#endif #include #include #include @@ -20,6 +18,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI getpid_nid_postfix(); +extern "C" std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -44,18 +43,8 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol return 0; } -std::string HostCpuModel() { -#if defined(__x86_64__) || defined(__i386__) - if (__get_cpuid_max(0x80000000, nullptr) < 0x80000004) return {}; - unsigned int brand[12] = {}; - for (unsigned int leaf = 0; leaf < 3; ++leaf) - __get_cpuid(0x80000002 + leaf, &brand[leaf * 4], &brand[leaf * 4 + 1], &brand[leaf * 4 + 2], &brand[leaf * 4 + 3]); - std::string model(reinterpret_cast(brand), strnlen(reinterpret_cast(brand), sizeof(brand))); - model.erase(0, model.find_first_not_of(' ')); - return model; -#else - return {}; -#endif +bool IsLeafMib(const int* name) { + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); } } @@ -66,6 +55,9 @@ std::int64_t APS5_VABI sysconf_nid_postfix(int name) { const int saved = *__error_nid_postfix(); std::int64_t result = -1; switch (name) { + case 3: // _SC_CLK_TCK + result = 128; + break; case 47: // _SC_PAGESIZE result = PS5_PAGE_SIZE; break; @@ -127,7 +119,7 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void if (model.empty()) throw std::runtime_error(std::string(__func__) + ": the host CPU has no CPUID brand string for hw.model"); return CopyOut(model.c_str(), model.size() + 1, oldValue, oldLength); } - if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (nameLength == 2 && name[0] == 6 && (name[1] == 5 || name[1] == 12)) { if (newValue != nullptr) { *__error_nid_postfix() = 1; return -1; @@ -138,14 +130,14 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { - if (nameLength != 4) { - *__error_nid_postfix() = 22; - return -1; - } if (newValue != nullptr) { *__error_nid_postfix() = 1; return -1; } + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } const int pid = name[3]; if (pid != -1 && pid != getpid_nid_postfix()) { *__error_nid_postfix() = 3; @@ -169,6 +161,10 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *oldLength = count; return 0; } + if (nameLength > 2 && IsLeafMib(name)) { + *__error_nid_postfix() = 20; + return -1; + } std::string mib; for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); @@ -184,8 +180,8 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si return -1; } if (std::strcmp(name, "kern.proc.pathname") == 0) { - *__error_nid_postfix() = 22; - return -1; + static constexpr int mib[] = {1, 14, 12}; + return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); } if (std::strcmp(name, "kern.arandom") == 0) { static constexpr int mib[] = {1, 37}; @@ -202,6 +198,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } + if (std::strcmp(name, "vm.overcommit") == 0) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": setting vm.overcommit"); + static constexpr int overcommit = 0; + return CopyOut(&overcommit, sizeof(overcommit), oldValue, oldLength); + } if (std::strcmp(name, "kern.sdk_version") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -210,6 +211,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr std::uint32_t sdkVersion = 0x01000000; return CopyOut(&sdkVersion, sizeof(sdkVersion), oldValue, oldLength); } + if (std::strcmp(name, "machdep.tsc_freq") == 0) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::uint64_t frequency = sceKernelGetTscFrequency(); + return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); + } if (std::strcmp(name, "hw.ncpu") == 0) { static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -222,6 +231,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 5}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } + if (std::strcmp(name, "hw.realmem") == 0) { + static constexpr int mib[] = {6, 12}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } @@ -229,9 +242,6 @@ int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* if (version == nullptr) { return SCE_KERNEL_ERROR_EFAULT; } - if (version->size != sizeof(SceKernelSystemSwVersion)) { - return SCE_KERNEL_ERROR_EINVAL; - } std::memset(version->versionString, 0, sizeof(version->versionString)); std::memcpy(version->versionString, "01.000.000", sizeof("01.000.000")); version->version = 0x01000000; diff --git a/core/libs/prx/libkernel/System/src/UserContext.cpp b/core/libs/prx/libkernel/System/src/UserContext.cpp index 30593de7c3..92eaa7a389 100644 --- a/core/libs/prx/libkernel/System/src/UserContext.cpp +++ b/core/libs/prx/libkernel/System/src/UserContext.cpp @@ -107,7 +107,7 @@ asm(".text\n" " mov %rax, 0x98(%rsp)\n" " fxsave64 0xa0(%rsp)\n" " mov %rsp, %rsi\n" - " call Aps5GetcontextFinish_nid_no_patch\n" + " call " APS5_ASM_SYMBOL("Aps5GetcontextFinish_nid_no_patch") "\n" " mov %rbp, %rsp\n" " pop %rbp\n" " lea 8(%rsp), %rsp\n" diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 6d57117e4e..8535253896 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -3,6 +3,7 @@ #include #include +#include #ifdef _WIN32 #include @@ -108,10 +109,11 @@ void SleepNanosCoarse(std::uint64_t nanos) { struct Waiter { HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr); HANDLE timer = CreateHighResolutionTimer(); - HANDLE sleepTimers[3]{}; + HANDLE sleepTimers[2]{}; Waiter* previous = nullptr; Waiter* next = nullptr; Waiter* nested = nullptr; + const void* key = nullptr; bool queued = false; bool busy = false; @@ -239,6 +241,111 @@ void Condition::NotifyAll() { } } +namespace { + +struct alignas(64) ParkBucket { + std::mutex lock; + Waiter* head = nullptr; + Waiter* tail = nullptr; + std::atomic parked{0}; +}; + +ParkBucket parkBuckets[256]; + +ParkBucket& ParkBucketFor(const void* key) { + return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; +} + +struct DeliveryDeferral { + DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } + ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } +}; + +void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { + if (waiter->previous) waiter->previous->next = waiter->next; + else bucket.head = waiter->next; + if (waiter->next) waiter->next->previous = waiter->previous; + else bucket.tail = waiter->previous; + waiter->previous = nullptr; + waiter->next = nullptr; + waiter->queued = false; +} + +bool LeavePark(ParkBucket& bucket, Waiter* waiter) { + bool signaled; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + signaled = !waiter->queued; + if (!signaled) UnlinkParked(bucket, waiter); + } + if (signaled) WaitForSingleObject(waiter->event, 0); + return signaled; +} + +void ReleaseParked(ParkBucket& bucket, Waiter* waiter) { + bucket.parked.fetch_sub(1, std::memory_order_seq_cst); + waiter->busy = false; + DrainApcs(); +} + +} // namespace + +void SpinPause() { + YieldProcessor(); +} + +Waiter* BeginPark(const void* key) { + auto& bucket = ParkBucketFor(key); + Waiter* waiter; + { + DeliveryDeferral deferral; + waiter = AcquireWaiter(); + waiter->key = key; + bucket.parked.fetch_add(1, std::memory_order_seq_cst); + std::lock_guard lock(bucket.lock); + waiter->queued = true; + waiter->next = nullptr; + waiter->previous = bucket.tail; + if (bucket.tail) bucket.tail->next = waiter; + else bucket.head = waiter; + bucket.tail = waiter; + } + DrainApcs(); + return waiter; +} + +bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos) { + auto& bucket = ParkBucketFor(key); + bool signaled = deadlineNanos ? WaitEventUntil(waiter, *deadlineNanos) : AlertableWait(1, &waiter->event, INFINITE) == WAIT_OBJECT_0; + if (!signaled) signaled = LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); + if (!signaled && !deadlineNanos) throw std::runtime_error("TimedWait: an untimed parked wait ended without a wake"); + return signaled; +} + +void CancelPark(const void* key, Waiter* waiter) { + auto& bucket = ParkBucketFor(key); + LeavePark(bucket, waiter); + ReleaseParked(bucket, waiter); +} + +void Unpark(const void* key) { + auto& bucket = ParkBucketFor(key); + if (bucket.parked.load(std::memory_order_seq_cst) == 0) return; + { + DeliveryDeferral deferral; + std::lock_guard lock(bucket.lock); + for (Waiter* waiter = bucket.head; waiter; waiter = waiter->next) { + if (waiter->key != key) continue; + UnlinkParked(bucket, waiter); + SetEvent(waiter->event); + break; + } + } + DrainApcs(); +} + #else void Condition::NotifyOne() { @@ -312,17 +419,6 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } -void PollSleepUntil(std::uint64_t deadlineNanos) { -#ifdef _WIN32 - const std::uint64_t now = NowNanos(); - if (deadlineNanos <= now) return; - const HANDLE timer = ThreadTimer(2); - if (!TimerWait(timer, deadlineNanos - now)) AlertableSleep(WholeMilliseconds(deadlineNanos - now)); -#else - SleepUntil(deadlineNanos); -#endif -} - void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 887ff0ebb8..4f7ccc32b5 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,38 +16,46 @@ 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); -void PollSleepUntil(std::uint64_t deadlineNanos); - -constexpr std::uint64_t LOCK_BULK_NANOS = 40000000ULL; -constexpr std::uint64_t LOCK_POLL_NANOS = 500000ULL; -constexpr std::uint64_t LOCK_TAIL_NANOS = 1000000ULL; - -template -bool AcquireUntil(std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { - if (Coarse()) return tryLockFor(RemainingMicros(deadlineNanos)); - for (;;) { - if (tryLock()) return true; - const std::uint64_t now = NowNanos(); - if (now >= deadlineNanos) return false; - const std::uint64_t remaining = deadlineNanos - now; -#ifdef _WIN32 - if (remaining > LOCK_TAIL_NANOS) { - PollSleepUntil(now + (remaining < LOCK_POLL_NANOS ? remaining : LOCK_POLL_NANOS)); - continue; - } -#else - if (remaining > LOCK_BULK_NANOS) { - if (tryLockFor((remaining - LOCK_BULK_NANOS) / 1000ULL)) return true; - continue; - } -#endif - if (remaining <= LOCK_TAIL_NANOS) SleepUntil(deadlineNanos); - else PollSleepUntil(now + LOCK_POLL_NANOS); - } -} struct Waiter; +#ifdef _WIN32 +constexpr unsigned PARK_SPINS = 1000; + +void SpinPause(); +Waiter* BeginPark(const void* key); +bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); +void CancelPark(const void* key, Waiter* waiter); +void Unpark(const void* key); + +template +bool ParkAcquire(const void* key, TTryLock tryLock, const std::uint64_t* deadlineNanos) { + for (unsigned spin = 0; spin < PARK_SPINS; ++spin) { + if (tryLock()) return true; + SpinPause(); + } + for (;;) { + Waiter* waiter = BeginPark(key); + if (tryLock()) { + CancelPark(key, waiter); + return true; + } + if (!FinishPark(key, waiter, deadlineNanos)) return tryLock(); + if (tryLock()) return true; + } +} +#endif + +template +bool AcquireUntil(const void* key, std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { +#ifdef _WIN32 + if (!Coarse()) return ParkAcquire(key, tryLock, &deadlineNanos); +#endif + static_cast(key); + static_cast(tryLock); + return tryLockFor(RemainingMicros(deadlineNanos)); +} + class Condition { public: Condition() = default; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index eb7ff910da..67c4823346 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -363,13 +363,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); std::error_code error; - const auto status = std::filesystem::symlink_status(source, error); + const auto status = std::filesystem::status(source, error); if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); - if (std::filesystem::is_symlink(status)) throw std::runtime_error("link: a symbolic link as the existing path is not supported"); if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(GuestEnoent)); - std::filesystem::create_hard_link(source, destination, error); + const auto target = std::filesystem::is_symlink(source, error) ? std::filesystem::canonical(source, error) : source; + if (!error) std::filesystem::create_hard_link(target, destination, error); if (error == std::errc::cross_device_link) return static_cast(Fail(GuestExdev)); if (error) throw std::system_error(error, "link"); RecordWrittenPath_nid_no_patch(destination); diff --git a/core/libs/tests/ApertureSources.cpp b/core/libs/tests/ApertureSources.cpp index f7e05eda1c..30fa83ebe4 100644 --- a/core/libs/tests/ApertureSources.cpp +++ b/core/libs/tests/ApertureSources.cpp @@ -4,6 +4,7 @@ #include "Translation/TranslationContext.hpp" #include #include +#include #include #include @@ -15,40 +16,36 @@ struct ApertureSource { std::uint32_t code; RdnaOperandKind kind; const char* name; + std::uint32_t low; + std::uint32_t high; }; const std::array ApertureSources{{ - {235u, RdnaOperandKind::SrcSharedBase, "src_shared_base"}, - {236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit"}, - {237u, RdnaOperandKind::SrcPrivateBase, "src_private_base"}, - {238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit"}, + {235u, RdnaOperandKind::SrcSharedBase, "src_shared_base", 0x00000000u, 0x80000000u}, + {236u, RdnaOperandKind::SrcSharedLimit, "src_shared_limit", 0xffffffffu, 0x80000000u}, + {237u, RdnaOperandKind::SrcPrivateBase, "src_private_base", 0x00000000u, 0x70000000u}, + {238u, RdnaOperandKind::SrcPrivateLimit, "src_private_limit", 0xffffffffu, 0x70000000u}, }}; +constexpr std::uint32_t MovB32 = 0x03u; +constexpr std::uint32_t MovB64 = 0x04u; + void Require(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); } -// The refusal must name the source it refused: a bare "not modelled" cannot be told from a decode gap. -template -void ExpectRefusal(const char* source, Fn translate) { - try { - translate(); - } catch (const std::exception& error) { - if (std::string(error.what()).find(source) == std::string::npos) { - throw std::runtime_error(std::string("the refusal does not name ") + source + ": " + error.what()); - } - return; - } - throw std::runtime_error(std::string("reading ") + source + " was accepted"); +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; } -// s_mov_b32 s0, : SOP1, format 0x7d, opcode 0x03, the source code in the low byte. -RdnaInstruction ScalarMove(std::uint32_t source) { - const std::array words{0x80000000u | (0x7du << 23u) | (0x03u << 8u) | source, 0u}; +RdnaInstruction ScalarMove(std::uint32_t opcode, std::uint32_t source) { + const std::array words{0x80000000u | (0x7du << 23u) | (opcode << 8u) | source, 0u}; return DecodeRdnaSop1(0u, words, 0u); } -void Translate(const RdnaInstruction& instruction) { +std::map TranslateWrites(const RdnaInstruction& instruction) { IrProgram program; auto& block = program.CreateBlock(); program.SetEntryBlock(block); @@ -56,6 +53,14 @@ void Translate(const RdnaInstruction& instruction) { program.Metadata().blockInfo.resize(1); TranslationContext context(program, block, 256); context.TranslateInstruction(instruction); + std::map writes; + for (const IrValue* value : block.Instructions()) { + if (value->Opcode() != IrOpcode::SetScalarRegister) continue; + const IrValue* written = value->Argument(1u)->Resolve(); + Require(written->HasImmediate(), "the write of s" + std::to_string(value->Argument(0u)->Register().index) + " is not a constant"); + writes[value->Argument(0u)->Register().index] = written->ImmediateU32(); + } + return writes; } void CheckDecode() { @@ -68,14 +73,27 @@ void CheckDecode() { } } -// Translating the move reads the source as a value, which is the path a title that carries the -// aperture into an address does not take and one that reads it does. -void CheckReadRefuses() { +void CheckWord(const std::map& writes, std::uint32_t reg, std::uint32_t expected, const std::string& read) { + const auto found = writes.find(reg); + Require(found != writes.end(), read + " did not write s" + std::to_string(reg)); + Require(found->second == expected, read + " wrote " + Hex(found->second) + " to s" + std::to_string(reg) + ", expected " + Hex(expected)); +} + +void CheckReadValues() { for (const auto& source : ApertureSources) { - const RdnaInstruction move = ScalarMove(source.code); - Require(move.op == RdnaOpcode::SMovB32, "the scalar move decoded to another opcode"); - Require(move.source0.kind == source.kind, "the scalar move decodes source0 as the wrong operand kind"); - ExpectRefusal(source.name, [&] { Translate(move); }); + const RdnaInstruction narrow = ScalarMove(MovB32, source.code); + Require(narrow.op == RdnaOpcode::SMovB32, "the 32-bit scalar move decoded to another opcode"); + Require(narrow.source0.kind == source.kind, "the 32-bit scalar move decodes source0 as the wrong operand kind"); + const auto narrowWrites = TranslateWrites(narrow); + CheckWord(narrowWrites, 0u, source.low, std::string("s_mov_b32 of ") + source.name); + Require(narrowWrites.size() == 1u, std::string("s_mov_b32 of ") + source.name + " wrote more than one register"); + const RdnaInstruction wide = ScalarMove(MovB64, source.code); + Require(wide.op == RdnaOpcode::SMovB64, "the 64-bit scalar move decoded to another opcode"); + Require(wide.source0.kind == source.kind, "the 64-bit scalar move decodes source0 as the wrong operand kind"); + const auto wideWrites = TranslateWrites(wide); + CheckWord(wideWrites, 0u, source.low, std::string("s_mov_b64 of ") + source.name); + CheckWord(wideWrites, 1u, source.high, std::string("s_mov_b64 of ") + source.name); + Require(wideWrites.size() == 2u, std::string("s_mov_b64 of ") + source.name + " wrote more than two registers"); } } @@ -83,7 +101,7 @@ void CheckReadRefuses() { int main() { CheckDecode(); - CheckReadRefuses(); + CheckReadValues(); std::puts("aperture sources tests passed"); return 0; } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index d463848a8a..43ce315163 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -51,10 +51,27 @@ FiberStorage g_secondStorage; auto* const g_first = reinterpret_cast(&g_firstStorage); auto* const g_second = reinterpret_cast(&g_secondStorage); +void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { + FiberInfo info{}; + info.size = sizeof(info); + Require(sceFiberGetInfo(fiber, &info) == 0, "active fiber context info"); + Require(info.size_context == expectedSize, "reported context keeps the requested size"); + volatile std::uint64_t marker = 0; + const auto address = reinterpret_cast(&marker); + const auto low = reinterpret_cast(info.addr_context); + const auto high = low + info.size_context; + Require(address >= low && address < high, "reported fiber context contains the executing stack"); + for (auto cursor = address; cursor < high; cursor += sizeof(std::uint64_t)) { + const volatile auto word = *reinterpret_cast(cursor); + (void)word; + } +} + void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 11 && argOnRun == 100, "first fiber arguments"); FiberObject* self = nullptr; Require(sceFiberGetSelf(&self) == 0 && self == g_first, "first fiber self"); + CheckActiveContext(g_first, g_firstContext.size()); std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null"); Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer"); @@ -65,6 +82,7 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) std::uint64_t received = 0; Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber"); Require(received == 300 && carried == 1.5, "switch back to first fiber"); + CheckActiveContext(g_first, g_firstContext.size()); for (;;) { Require(sceFiberReturnToThread(received + 1, &received) == 0, "first fiber return"); } @@ -72,10 +90,12 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) void APS5_VABI SecondEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) { Require(argOnInitialize == 22 && argOnRun == 200, "second fiber arguments"); + CheckActiveContext(g_second, g_secondContext.size()); Require(sceFiberSwitch(g_second, 0, nullptr) == FiberErrorState, "switch to self"); _mm_setcsr(_mm_getcsr() | 0x8000u); std::uint64_t received = 0; Require(sceFiberReturnToThread(250, &received) == 0 && received == 260, "second fiber resumed on another thread"); + CheckActiveContext(g_second, g_secondContext.size()); Require((_mm_getcsr() & 0x8000u) != 0, "second fiber keeps its MXCSR"); Require(sceFiberSwitch(g_first, 300, nullptr) == 0, "switch to first fiber"); Require(false, "second fiber resumed after its last switch"); @@ -143,6 +163,7 @@ void CheckWindowsHostStack() { #endif void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { + CheckActiveContext(reinterpret_cast(&g_hostCallStorage), HostCallContextSize); #ifdef _WIN32 CheckWindowsHostStack(); #endif diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index d0431836ba..ff80543263 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -8,6 +8,12 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); @@ -39,6 +45,18 @@ static void ExpectException(TAction action) { throw std::runtime_error("Expected a stream exception"); } +static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) { +#ifdef _WIN32 + const int descriptor = _open(path, _O_RDONLY | _O_BINARY); + Require(descriptor >= 0 && _dup2(descriptor, _fileno(GetNativeStream(stream))) == 0); + Require(_close(descriptor) == 0); +#else + const int descriptor = open(path, O_RDONLY); + Require(descriptor >= 0 && dup2(descriptor, fileno(GetNativeStream(stream))) >= 0); + Require(close(descriptor) == 0); +#endif +} + int main(int argc, char** argv) { Require(argc == 2); Require(_Stdout_nid_postfix.GetHandle() == stdout); @@ -75,6 +93,19 @@ int main(int argc, char** argv) { errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); Require(errno == ENOENT); +#ifdef _WIN32 + const std::string directoryMode = "rb"; + constexpr int directoryError = EACCES; +#else + const std::string directoryMode = "wb"; + constexpr int directoryError = EISDIR; +#endif + const std::string directoryName = std::string(argv[1]) + ".dir"; + Require(std::filesystem::create_directory(directoryName)); + errno = 0; + Require(fopen_nid_postfix(directoryName.c_str(), directoryMode.c_str()) == nullptr); + Require(errno == directoryError); + Require(std::filesystem::remove(directoryName)); const std::u8string unicodeName = u8"セーブé.tmp"; const std::string guestName(unicodeName.begin(), unicodeName.end()); const std::filesystem::path hostName(unicodeName); @@ -131,6 +162,12 @@ int main(int argc, char** argv) { Require(fputs_nid_postfix(marker, stream) == EOF); Require(ferror_nid_postfix(stream) != 0); Require(fclose_nid_postfix(stream) == 0); + stream = fopen_nid_postfix(argv[1], "wb"); + Require(stream != nullptr && fputs_nid_postfix(marker, stream) >= 0); + ReplaceWithReadOnlyDescriptor(stream, argv[1]); + Require(fflush_nid_postfix(stream) == EOF); + Require(ferror_nid_postfix(stream) != 0); + static_cast(fclose_nid_postfix(stream)); Require(std::remove(argv[1]) == 0); std::cout << "PASS: stream objects, file operations, EOF and error handling\n"; } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index b03e0a14fb..5d0dc75362 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -496,6 +496,53 @@ bool Refused(std::uint32_t context, const AjmBatchInfo& info) { return false; } +void TestOpusMonoFamily255(std::uint32_t context) { + std::uint32_t instances[2] = {}; + for (std::size_t index = 0; index < 2; ++index) { + Require(sceAjmInstanceCreate(context, 24, 0, &instances[index]) == 0); + const std::uint32_t parameters[3] = {1, 48000, index == 0 ? 0u : 255u}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instances[index], parameters, sizeof(parameters), initResult) == 0); + Submit(context, info); + Require(initResult[0] == 0); + } + std::size_t offset = 0; + int peak = 0; + for (int packet = 0; packet < 3; ++packet) { + const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u); + std::vector pcm[2] = {std::vector(960, -12345), std::vector(960, -12345)}; + for (std::size_t index = 0; index < 2; ++index) { + DecodeSideband sideband{}; + RunDecode(context, instances[index], OPUS_STEREO + offset, 2 + bytes, pcm[index].data(), pcm[index].size() * sizeof(std::int16_t), sideband); + Require(sideband.result == 0 && static_cast(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2); + Require(sideband.totalDecodedSamples == static_cast(packet + 1) * 960); + } + Require(pcm[0] == pcm[1]); + if (packet > 0) { + for (const std::int16_t sample : pcm[1]) peak = std::max(peak, std::abs(static_cast(sample))); + } + offset += 2 + bytes; + } + Require(peak > 900 && peak < 1200); + + const std::uint32_t refusedParameters[2][3] = {{2, 48000, 255}, {1, 48000, 2}}; + for (const auto& parameters : refusedParameters) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t initResult[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0); + Require(Refused(context, info)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + } + for (const std::uint32_t instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestDecodeSingle(std::uint32_t context) { std::uint32_t instance = 0; Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); @@ -1245,6 +1292,7 @@ int main() { TestMp3(context); TestOpus(context); TestOpus(context, 1); + TestOpusMonoFamily255(context); TestOpusFamilyZeroChannels(context); TestOpusSurround(context, 3, 0); for (std::size_t stream = 0; stream < 2; ++stream) { diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp new file mode 100644 index 0000000000..22629e0166 --- /dev/null +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -0,0 +1,197 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#endif + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t); +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI aligned_alloc_nid_postfix(std::size_t, std::size_t); +int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int NoMemory = 12; +constexpr int InvalidArgument = 22; +constexpr std::size_t Max = std::numeric_limits::max(); +constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits::max()) + 1; + +int failures = 0; + +void report(const char* what, const char* detail) { + std::fprintf(stderr, "%s: %s\n", what, detail); + ++failures; +} + +template +void expectNull(const char* what, int expectedError, TCall call) { + *__error_nid_postfix() = 0; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s instead of returning NULL\n", what, error.what()); + ++failures; + return; + } catch (...) { + report(what, "threw a non-standard exception instead of returning NULL"); + return; + } + if (result != nullptr) { + report(what, "returned a block"); + free_nid_postfix(result); + return; + } + if (*__error_nid_postfix() != expectedError) { + std::fprintf(stderr, "%s: errno %d, expected %d\n", what, *__error_nid_postfix(), expectedError); + ++failures; + } +} + +template +void* expectBlock(const char* what, TCall call) { + *__error_nid_postfix() = 77; + void* result = nullptr; + try { + result = call(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: threw %s\n", what, error.what()); + ++failures; + return nullptr; + } + if (result == nullptr) report(what, "returned NULL"); + if (*__error_nid_postfix() != 77) report(what, "changed errno on success"); + return result; +} + +} + +int main() { + void* api[10]{}; + ApplicationHeapRegister_nid_no_patch(api); + + expectNull("malloc(SIZE_MAX)", NoMemory, [] { return malloc_nid_postfix(Max); }); + expectNull("malloc(PTRDIFF_MAX + 1)", NoMemory, [] { return malloc_nid_postfix(HalfPlusOne); }); + expectNull("calloc(SIZE_MAX, 2)", NoMemory, [] { return calloc_nid_postfix(Max, 2); }); + expectNull("calloc(2^33, 2^33)", NoMemory, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); + expectNull("calloc(PTRDIFF_MAX + 1, 1)", NoMemory, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); + expectNull("realloc(NULL, SIZE_MAX)", NoMemory, [] { return realloc_nid_postfix(nullptr, Max); }); + expectNull("aligned_alloc(64, SIZE_MAX)", NoMemory, [] { return aligned_alloc_nid_postfix(64, Max); }); + expectNull("memalign(64, PTRDIFF_MAX + 1)", NoMemory, [] { return memalign_nid_postfix(64, HalfPlusOne); }); + expectNull("aligned_alloc(24, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(24, 64); }); + expectNull("aligned_alloc(0, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(0, 64); }); + expectNull("memalign(24, 64)", InvalidArgument, [] { return memalign_nid_postfix(24, 64); }); + + auto* block = static_cast(expectBlock("malloc(40)", [] { return malloc_nid_postfix(40); })); + if (block != nullptr) { + std::memset(block, 0x6b, 40); + expectNull("realloc(block, SIZE_MAX)", NoMemory, [block] { return realloc_nid_postfix(block, Max); }); + expectNull("realloc(block, PTRDIFF_MAX + 1)", NoMemory, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); + for (unsigned index = 0; index < 40; ++index) { + if (block[index] != 0x6b) report("realloc failure", "damaged the original block"); + } + free_nid_postfix(block); + } + + if (GuestArena::GuestArenaAvailable_nid_postfix()) { +#ifdef __linux__ + rlimit originalLimit{}; + if (getrlimit(RLIMIT_DATA, &originalLimit) != 0) { + report("failed commit setup", "could not read RLIMIT_DATA"); + return 1; + } + auto limited = originalLimit; + limited.rlim_cur = std::min(limited.rlim_cur, rlim_t{128} << 20); + if (setrlimit(RLIMIT_DATA, &limited) != 0) { + report("failed commit setup", "could not limit RLIMIT_DATA"); + return 1; + } +#endif + constexpr std::size_t beyondCommitLimit = std::size_t{300} << 30; + constexpr std::size_t probeBytes = std::size_t{100} << 30; + void* before = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(before, probeBytes); + bool committed = false; + int failedCommits = 0; + for (int attempt = 0; attempt < 10 && !committed; ++attempt) { + *__error_nid_postfix() = 0; + void* huge = nullptr; + try { + huge = malloc_nid_postfix(beyondCommitLimit); + } catch (const std::exception& error) { + std::fprintf(stderr, "malloc(300 GiB): threw %s\n", error.what()); + ++failures; + break; + } + if (huge != nullptr) { + free_nid_postfix(huge); + committed = true; + } else if (*__error_nid_postfix() != NoMemory) { + report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } else { + ++failedCommits; + } + } +#ifdef __linux__ + if (setrlimit(RLIMIT_DATA, &originalLimit) != 0) report("failed commit cleanup", "could not restore RLIMIT_DATA"); +#endif + if (committed) { +#ifdef __linux__ + report("malloc(300 GiB)", "succeeded despite RLIMIT_DATA"); +#else + std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); +#endif + } else { + if (failedCommits != 10) report("failed heap commits", "did not return ENOMEM for all ten attempts"); + void* after = nullptr; + try { + after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); + GuestArena::GuestArenaRelease_nid_postfix(after, probeBytes); + } catch (const std::exception& error) { + std::fprintf(stderr, "failed commit range probe: %s\n", error.what()); + ++failures; + } + if (after != before) report("malloc(300 GiB)", "left its arena range reserved after failing"); + std::fprintf(stderr, "Checked %d failed heap commits and arena range reuse\n", failedCommits); + } + void* small = malloc_nid_postfix(100); + if (small == nullptr) report("malloc(100) after a failed huge malloc", "returned NULL"); + free_nid_postfix(small); + void* large = malloc_nid_postfix(std::size_t{1} << 20); + if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); + free_nid_postfix(large); + } + + void* untouched = &failures; + *__error_nid_postfix() = 77; + if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, HalfPlusOne) != NoMemory) report("posix_memalign(64, PTRDIFF_MAX + 1)", "did not return ENOMEM"); + if (posix_memalign_nid_postfix(&untouched, 64, Max) != NoMemory) report("posix_memalign(64, SIZE_MAX)", "did not return ENOMEM"); + if (untouched != &failures || *__error_nid_postfix() != 77) report("posix_memalign failure", "touched the output or errno"); + + free_nid_postfix(expectBlock("calloc(3, 5)", [] { return calloc_nid_postfix(3, 5); })); + free_nid_postfix(expectBlock("calloc(0, 5)", [] { return calloc_nid_postfix(0, 5); })); + free_nid_postfix(expectBlock("aligned_alloc(128, 1)", [] { return aligned_alloc_nid_postfix(128, 1); })); + free_nid_postfix(expectBlock("memalign(4096, 1)", [] { return memalign_nid_postfix(4096, 1); })); + void* first = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + void* second = expectBlock("malloc(0)", [] { return malloc_nid_postfix(0); }); + if (first != nullptr && first == second) report("malloc(0)", "returned the same block twice"); + free_nid_postfix(first); + free_nid_postfix(second); + + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2845913f5c..e7e80e9e27 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -651,6 +651,70 @@ void TestZeroFilledBuffer() { std::remove(path); } +void TestReadPastEndOfFileZeroFills() { + const std::filesystem::path directory = "ampr_read_past_end"; + std::filesystem::remove_all(directory); + Require(std::filesystem::create_directory(directory)); + const auto file = directory / "settings.ini"; + { + std::ofstream stream(file, std::ios::binary); + for (std::size_t offset = 0; offset < 64; ++offset) stream.put(static_cast(FileByte(offset))); + } + const std::string filePath = file.generic_string(); + const char* path = filePath.c_str(); + std::uint32_t fileId = 0; + std::uint64_t size = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIdsAndFileSizes(&path, 1, &fileId, &size, &failed) == 0 && size == 64); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + for (std::size_t offset = 1; offset < size; ++offset) stream.put(static_cast(FileByte(offset))); + } + + std::array whole; + std::array continued; + std::array atEnd; + std::array pastEnd; + whole.fill(0xA5); + continued.fill(0xA5); + atEnd.fill(0xA5); + pastEnd.fill(0xA5); + Recorder recorder; + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, whole.data(), size, 0) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, continued.data(), 16) == 0); + Require(sceAmprAprCommandBufferReadFileGather(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, 8, 0) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, atEnd.data(), atEnd.size(), size - 1) == 0); + Require(sceAmprAprCommandBufferReadFile(&recorder.buffer, &recorder.gatherState, &recorder.scatterState, fileId, pastEnd.data(), pastEnd.size(), 4096) == 0); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + Require(MatchesFile(whole.data(), 1, size - 1) && whole[size - 1] == 0); + for (std::size_t index = 0; index < 16; ++index) Require(continued[index] == 0); + Require(MatchesFile(continued.data() + 16, 1, 8)); + for (const auto byte : atEnd) Require(byte == 0); + for (const auto byte : pastEnd) Require(byte == 0); + std::filesystem::remove_all(directory); +} + +void TestReadAtUnseekableOffsetThrows() { + const char* path = "ampr_unseekable_offset.bin"; + { std::ofstream file(path, std::ios::binary); file << "data"; } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + std::array data{}; + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, fileId, 0, reinterpret_cast(data.data()), data.size(), std::uint64_t{1} << 63u}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("returned 0 of 4 bytes") != std::string::npos; + } + Require(rejected); + std::remove(path); +} + void TestResolveFailures() { const char* paths[]{"ampr_resolve_existing.bin", "ampr_resolve_missing.bin"}; { std::ofstream file(paths[0], std::ios::binary); file << "data"; } @@ -1403,6 +1467,8 @@ int main() { TestClearBuffer(); TestConstructed(); TestZeroFilledBuffer(); + TestReadPastEndOfFileZeroFills(); + TestReadAtUnseekableOffsetThrows(); TestResolveFailures(); TestMalformedCommandSizes(); TestResolveSymbolicLink(); diff --git a/core/libs/tests/GuestAmprCppError.S b/core/libs/tests/GuestAmprCppError.S index 024758cd41..f5e0769766 100644 --- a/core/libs/tests/GuestAmprCppError.S +++ b/core/libs/tests/GuestAmprCppError.S @@ -3,8 +3,13 @@ .globl GuestCatch GuestCatch: .cfi_startproc +#ifdef _WIN32 .cfi_personality 0x00, __gxx_personality_v0_nid_postfix .cfi_lsda 0x00, .Lcatch_lsda +#else + .cfi_personality 0x9b, .Lcatch_personality + .cfi_lsda 0x1b, .Lcatch_lsda +#endif pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 @@ -34,10 +39,18 @@ GuestCatch: #ifdef _WIN32 .section .rdata,"dr" #else +.section .data.rel.ro,"aw",@progbits +.p2align 3 +.Lcatch_personality: + .quad __gxx_personality_v0_nid_postfix .section .rodata #endif .Lcatch_lsda: +#ifdef _WIN32 .byte 0xff, 0x80 +#else + .byte 0xff, 0x9b +#endif .uleb128 .Lcatch_types_end-.Lcatch_types_offset .Lcatch_types_offset: .byte 0x01 @@ -49,8 +62,13 @@ GuestCatch: .uleb128 1 .Lcatch_calls_end: .byte 1, 1, 2, 0 +#ifdef _WIN32 .quad FixtureCatchType .quad FixtureOtherType +#else + .long FixtureCatchType-. + .long FixtureOtherType-. +#endif .Lcatch_types_end: #ifndef _WIN32 .section .note.GNU-stack,"",@progbits diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 1f68eaec06..8fb8a16677 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBoot int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -85,6 +86,8 @@ int main() { Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); Require(sceAppContentAddcontEnqueueDownload(0, &label) == ErrorNotFound); Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontDelete(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontDelete(7, nullptr) == ErrorParameter); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == ErrorParameter); Require(sceAppContentAddcontMount(0, &label, &mountPoint) == ErrorNotFound); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index d6ca109955..0410bbab96 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1070,6 +1070,30 @@ void TestAutoStartKeepsStartedPlayback() { Check(startedByTitle, "auto-start won every race against the title's own start"); } +void TestRestartAfterPausedJumpKeepsClockRunning() { + Events events; + AvPlayerInitData 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(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no video after start"); + Check(sceAvPlayerPause(player) == 0, "pause failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "looping rejected"); + Check(sceAvPlayerJumpToTime(player, 0) == 0, "jump while paused failed"); + Check(sceAvPlayerStart(player) == 0, "restart after the paused jump failed"); + std::uint64_t last = 0; + Check(WaitFor([&] { + if (sceAvPlayerGetVideoDataEx(player, &frame)) last = frame.timestamp; + return last >= 200; + }), "playback froze after a restart that followed a paused jump"); + Check(sceAvPlayerCurrentTime(player) > 0, "clock stayed frozen after the restart"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { Events events; AvPlayerInitData init = InitData(&events); @@ -1168,6 +1192,7 @@ int main() { TestHandedOutFramesStayIntact(); TestPs5ExtendedInitLayout(); TestAutoStartKeepsStartedPlayback(); + TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); TestEofStopLatency(); std::puts("AvPlayer tests passed"); diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index f15d13e2ec..76b09054a6 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -26,6 +26,7 @@ int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); Pthread APS5_VABI pthread_self_nid_postfix(void); int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset); +int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset); int APS5_VABI scePthreadGetthreadid(void); } @@ -46,6 +47,7 @@ struct RestoreProcessMask { static constexpr std::size_t GUEST_CPUSET_BYTES = 32; static constexpr int GUEST_ESRCH = 3; +static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_ERANGE = 34; @@ -236,6 +238,39 @@ int main() { Require(unionResult == 0 && unionSet[0] == (first | last)); Require(ownResult == 0 && ownSet[0] == last); } + std::uint64_t request[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {first, 0, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(pthread_self_nid_postfix()) == first); + std::promise settle; + auto settled = settle.get_future(); + Pthread target = nullptr; + Require(scePthreadCreate(&target, &attr, Worker, &settled, nullptr) == SCE_OK); + request[0] = last; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == last && ReadCpuset(pthread_self_nid_postfix()) == first); + request[0] = available | (1ull << 40); + request[3] = 1ull << 63; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == 0); + Require(ReadCpuset(target) == available); + const std::uint64_t outside[GUEST_CPUSET_BYTES / sizeof(std::uint64_t)] = {1ull << 40, 1, 0, 0}; + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, outside) == GUEST_EDEADLK); + Require(ReadCpuset(target) == available && ReadCpuset(pthread_self_nid_postfix()) == first); + for (const std::size_t size : {std::size_t{0}, std::size_t{8}, std::size_t{31}, std::size_t{33}, std::size_t{64}}) { + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), size, request) == GUEST_ERANGE); + Require(pthread_setaffinity_np_nid_postfix(target, size, request) == GUEST_ERANGE); + } + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, nullptr) == GUEST_EFAULT); + Require(pthread_setaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, request) == GUEST_EINVAL); + std::uint64_t unknownThread[8]{}; + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + Require(pthread_setaffinity_np_nid_postfix(reinterpret_cast(unknownThread), 8, request) == GUEST_ESRCH); + settle.set_value(); + Require(scePthreadJoin(target, nullptr) == SCE_OK); + Require(pthread_setaffinity_np_nid_postfix(target, GUEST_CPUSET_BYTES, request) == GUEST_ESRCH); + request[0] = available; + request[3] = 0; + Require(pthread_setaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, request) == 0); Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(ReadCpuset(pthread_self_nid_postfix()) == available); diff --git a/core/libs/tests/GuestCes.cpp b/core/libs/tests/GuestCes.cpp index 55bd867420..b37607ad73 100644 --- a/core/libs/tests/GuestCes.cpp +++ b/core/libs/tests/GuestCes.cpp @@ -9,6 +9,7 @@ const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1251(void); int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); int APS5_VABI sceCesSbcToUtf8(const std::uint8_t*, std::uint8_t, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8ToSbc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const std::uint8_t*, std::uint8_t*); +void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -106,4 +107,9 @@ int main() { char context[256] = {}; Require(sceCesMbcsUcsContextInit(context, cyrillic) == 0); Require(sceCesMbcsUcsContextInit(nullptr, cyrillic) == invalidParameter); + + char sheet[256] = {}; + Require(sceCesUcsProfileInitSJis1997Cp932(sheet) == sheet); + Require(sceCesUcsProfileInitSJis1997Cp932(nullptr) == nullptr); + Require(sceCesMbcsUcsContextInit(context, sheet) == 0); } diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp new file mode 100644 index 0000000000..c25a9f31bd --- /dev/null +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -0,0 +1,30 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + constexpr int invalidAddress = static_cast(0x80BC0031u); + constexpr int invalidUserId = static_cast(0x80BC0010u); + + Require(sceConvertKeycodeGetImeKeyboardType(0, nullptr) == invalidAddress); + Require(sceConvertKeycodeGetImeKeyboardType(-1, nullptr) == invalidAddress); + + std::uint32_t type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(-1, &type) == invalidUserId); + Require(type == 0xFFFFFFFFu); + + Require(sceConvertKeycodeGetImeKeyboardType(0, &type) == 0); + Require(type == 0); + + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); + Require(type == 0); + return 0; +} diff --git a/core/libs/tests/GuestFileLock.cpp b/core/libs/tests/GuestFileLock.cpp index eea4421195..5cd80d4496 100644 --- a/core/libs/tests/GuestFileLock.cpp +++ b/core/libs/tests/GuestFileLock.cpp @@ -20,6 +20,9 @@ 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 flock_nid_postfix(int, int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int* APS5_VABI __error_nid_postfix(); FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); FileStream* APS5_VABI fdopen_nid_postfix(int, const char*); @@ -165,6 +168,12 @@ static void Conversions() { Require(flock_nid_postfix(copy, 2 | 4) == -1 && *__error_nid_postfix() == 35); Require(flock_nid_postfix(fixture.other, 8) == 0); Require(Acquires(fixture.other, 2)); + Require(flock_nid_postfix(fixture.source, 2 | 4) == 0); + char byte = 'x'; + Require(sceKernelPwrite(fixture.other, &byte, 1, 0) == 1 && sceKernelWrite(fixture.other, "yz", 2) == 2); + Require(sceKernelPread(fixture.other, &byte, 1, 1) == 1 && byte == 'z'); + Require(flock_nid_postfix(fixture.other, 1 | 4) == -1 && *__error_nid_postfix() == 35); + Require(flock_nid_postfix(fixture.source, 8) == 0); } } diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 370be6be8f..76f392261f 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -45,6 +45,8 @@ std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelFtruncate(int, std::int64_t); } @@ -176,6 +178,14 @@ int main() { Require(sceKernelPwrite(file, nullptr, 1, 0) == ErrorEfault); Require(sceKernelPread(file, &byte, 1, -1) == ErrorEinval); Require(sceKernelPwrite(file, &byte, 1, -1) == ErrorEinval); + Require(pread_nid_postfix(file, &byte, 1, 3) == 1 && byte == 'B'); + Require(pread_nid_postfix(file, nullptr, 0, 0) == 0); + Require(pread_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pwrite_nid_disambig1_nid_postfix(file, nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); + Require(pread_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pwrite_nid_disambig1_nid_postfix(file, &byte, 1, -1) == -1 && *__error_nid_postfix() == 22); + Require(pread_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); + Require(pwrite_nid_disambig1_nid_postfix(9999, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFtruncate(file, -1) == ErrorEinval); const int readOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); @@ -185,6 +195,7 @@ int main() { const int writeOnly = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_WRONLY, 0); Require(writeOnly >= 0); Require(sceKernelPread(writeOnly, &byte, 1, 0) == ErrorEbadf); + Require(pread_nid_postfix(writeOnly, &byte, 1, 0) == -1 && *__error_nid_postfix() == 9); Require(sceKernelClose(writeOnly) == 0); Require(Contents(path) == "01ABCDE7ABCDE"); Require(sceKernelFtruncate(file, 5) == 0); @@ -206,6 +217,15 @@ int main() { Require(sceKernelWritev(ends[1], message, 1) == 3); Require(sceKernelReadv(ends[0], reads, 2) == 3); Require(std::memcmp(first, "xyz", 3) == 0); + char stuvw[] = "stuvw"; + GuestIovec longer[1] = {{stuvw, 5}}; + Require(sceKernelWritev(ends[1], longer, 1) == 5); + Require(sceKernelReadv(ends[0], reads, 2) == 5); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vw", 2) == 0); + Require(sceKernelWritev(ends[1], longer, 1) == 5 && sceKernelWritev(ends[1], longer, 1) == 5); + Require(readv_nid_postfix(ends[0], reads, 2) == 7); + Require(std::memcmp(first, "stu", 3) == 0 && std::memcmp(second, "vwst", 4) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == 3 && std::memcmp(first, "uvw", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); #ifndef _WIN32 diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 987e09890d..854cf0fe33 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -27,6 +27,7 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char*, unsigned short); 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 futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -51,6 +52,11 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelOpen(const char*, int, unsigned short); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +std::int64_t APS5_VABI sceKernelReadv(int, const void*, int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPreadv(int, const void*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); int APS5_VABI sceKernelStat(const char*, FileStat*); int APS5_VABI sceKernelCheckReachability(const char*); int APS5_VABI sceKernelFstat(int, FileStat*); @@ -93,6 +99,15 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags, const char* names) { + try { + chflags_nid_postfix(path.string().c_str(), flags); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), names) != nullptr; + } + return false; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -155,6 +170,19 @@ 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); +#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); + Require(remove_nid_postfix("/parent-test-mount/slash-remove/") == -1 && *__error_nid_postfix() == 20); + Require(rename_nid_postfix("/parent-test-mount/slash-rename/", "/parent-test-mount/slash-renamed") == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix("/parent-test-mount/slash-open/", 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelUnlink("/parent-test-mount/slash-kernel/") == static_cast(0x80020014u)); + for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) Require(std::filesystem::is_regular_file(root / name)); + Require(!std::filesystem::exists(root / "slash-renamed")); + Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000); + Require(mkdir_nid_postfix("/parent-test-mount/slash-directory/", 0755) == 0 && std::filesystem::is_directory(root / "slash-directory")); + Require(rmdir_nid_postfix("/parent-test-mount/slash-directory/") == 0 && !std::filesystem::exists(root / "slash-directory")); +#endif RemovePathAlias_nid_no_patch("parent-test-mount"); std::error_code linkError; std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); @@ -202,14 +230,33 @@ static void TestRelinkedModuleNames() { std::filesystem::remove_all(root); } +static void TestRelinkedModuleNameCase() { + const auto root = std::filesystem::path("anyps5-relinked-case-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + { std::ofstream file(root / "librenoircore.ps5.prx.guest.prx"); file << "relinked"; } + const auto requested = (root / "libRenoirCore.PS5.prx").generic_string(); + FileStat status{}; + Require(sceKernelStat(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(stat_nid_postfix(requested.c_str(), &status) == 0 && status.st_size == 8); + Require(sceKernelCheckReachability(requested.c_str()) == 0); + const int opened = sceKernelOpen(requested.c_str(), 0, 0); + Require(opened >= 0 && sceKernelClose(opened) == 0); + Require(sceKernelStat((root / "libRenoirCore.PS5.prx2").generic_string().c_str(), &status) == static_cast(0x80020002u)); + std::filesystem::remove_all(root); +} + int main() { TestParentTraversal(); TestRelinkedModuleNames(); + TestRelinkedModuleNameCase(); Require(chmod_nid_postfix(nullptr, 0600) == -1 && *__error_nid_postfix() == 14); Require(sceKernelChmod_nid_postfix(nullptr, 0600) == static_cast(0x8002000eu)); Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2); Require(chmod_nid_postfix("anyps5_missing_chmod_path", 0600) == -1 && *__error_nid_postfix() == 2); Require(sceKernelChmod_nid_postfix("anyps5_missing_chmod_path", 0600) == static_cast(0x80020002u)); + Require(sceKernelStat("anyps5_missing_chmod_path", nullptr) == static_cast(0x80020002u)); + Require(sceKernelStat(".", nullptr) == static_cast(0x8002000eu)); Require(sceKernelDebugOutText(-1, "text") == static_cast(0x80020016u)); std::uint64_t throttling[4] = {1, 2, 3, 4}; Require(sceKernelWriteThrottlingStatus(throttling) == 0); @@ -269,6 +316,20 @@ int main() { Require(created >= 0 && close_nid_postfix(created) == 0 && std::filesystem::file_size("creat_probe.bin") == 0); Require(std::filesystem::remove("creat_probe.bin")); } + { + int ends[2] = {-1, -1}; + Require(pipe_nid_postfix(ends) == 0 && write_nid_postfix(ends[1], "abc", 3) == 3); + for (const int end : ends) { + Require(sceKernelFtruncate(end, 0) == static_cast(0x80020016u) && sceKernelFtruncate(end, 8) == static_cast(0x80020016u)); + } + char kept[4]{}; + Require(read_nid_postfix(ends[0], kept, sizeof(kept)) == 3 && std::memcmp(kept, "abc", 3) == 0); + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + for (const char* name : {".", "/dev/urandom"}) { + const int opened = open_nid_postfix(name, 0, 0); + Require(opened >= 0 && sceKernelFtruncate(opened, 0) == static_cast(0x80020016u) && close_nid_postfix(opened) == 0); + } + } Require(close_nid_postfix(descriptors[0]) == 0); auto* hostCapture = std::tmpfile(); Require(hostCapture != nullptr && pipe_nid_postfix(descriptors) == 0); @@ -329,6 +390,20 @@ int main() { Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; { std::ofstream stream(file); stream << "retained until removal"; } + Require(chflags_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + Require(chflags_nid_postfix("", 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), 0) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((root / "missing").string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 2); + Require(chflags_nid_postfix((file / "child").string().c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix((file.string() + "/").c_str(), 0) == -1 && *__error_nid_postfix() == 20); + Require(chflags_nid_postfix(file.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(root.string().c_str(), 0) == 0); + Require(chflags_nid_postfix(file.string().c_str(), 0x20) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 1ULL << 32) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), ~0ULL) == -1 && *__error_nid_postfix() == 45); + Require(chflags_nid_postfix(file.string().c_str(), 0x200000) == -1 && *__error_nid_postfix() == 1); + Require(ChflagsThrows(file, 0x2, "UF_IMMUTABLE")); + Require(ChflagsThrows(file, 0x1000 | 0x40000, "UF_READONLY|SF_APPEND")); const auto held = root / "held.txt"; const auto heldName = held.string(); { std::ofstream stream(held); stream << "held"; } @@ -690,7 +765,18 @@ int main() { Require(syncedLocked >= 0 && sceKernelFsync(syncedLocked) == 0 && close_nid_postfix(syncedLocked) == 0); Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0600) == 0); const int directoryDescriptor = open_nid_postfix(rootName.c_str(), 0, 0); - Require(directoryDescriptor >= 0 && close_nid_postfix(directoryDescriptor) == 0); + Require(directoryDescriptor >= 0); + char directoryBytes[8]{}; + struct { void* base; std::size_t length; } directoryVector[2] = {{directoryBytes, 3}, {directoryBytes + 3, 5}}; + Require(read_nid_postfix(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == -1 && *__error_nid_postfix() == 21); + Require(sceKernelRead(directoryDescriptor, directoryBytes, sizeof(directoryBytes)) == static_cast(0x80020015u)); + Require(sceKernelReadv(directoryDescriptor, directoryVector, 2) == static_cast(0x80020015u)); + Require(sceKernelPread(directoryDescriptor, directoryBytes, sizeof(directoryBytes), 0) == static_cast(0x80020015u)); + Require(sceKernelPreadv(directoryDescriptor, directoryVector, 2, 0) == static_cast(0x80020015u)); + Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); + std::vector listing(4096); + Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); + Require(close_nid_postfix(directoryDescriptor) == 0); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); Require(flaggedDirectory >= 0 && sceKernelClose(flaggedDirectory) == 0); std::vector entries(4096); diff --git a/core/libs/tests/GuestFlexibleMemoryBudget.cpp b/core/libs/tests/GuestFlexibleMemoryBudget.cpp new file mode 100644 index 0000000000..7938cd2a8c --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryBudget.cpp @@ -0,0 +1,89 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelConfiguredFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); +std::uint64_t GuestImageSegmentBytes_nid_no_patch(); +void* APS5_VABI dlopen_nid_postfix(const char*, int); +char* APS5_VABI dlerror_nid_postfix(); +int APS5_VABI dlclose_nid_postfix(void*); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Flexible memory budget check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +int main(int argc, char** argv) { + Require(argc == 2); + const std::string_view mode = argv[1]; + Require(sceKernelConfiguredFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelAvailableFlexibleMemorySize(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::size_t configured = 0; + if (mode == "invalid") { + bool rejected = false; + try { sceKernelConfiguredFlexibleMemorySize(&configured); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected && configured == 0); + return 0; + } + const std::size_t expected = (mode == "declared" ? 800u : mode == "limited" ? 16u : 448u) * 1024u * 1024u; + Require(sceKernelConfiguredFlexibleMemorySize(&configured) == 0 && configured == expected); + std::size_t before = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured); + void* module = nullptr; + if (mode == "declared") { + const std::filesystem::path fixture = GUEST_FLEXIBLE_MEMORY_MODULE; + AddPathAlias_nid_no_patch("budget_module", fixture.parent_path().string().c_str()); + const auto path = "/budget_module/" + fixture.filename().generic_string(); + module = dlopen_nid_postfix(path.c_str(), 2); + if (!module) std::fprintf(stderr, "Cannot load budget fixture: %s\n", dlerror_nid_postfix()); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + Require(GuestImageSegmentBytes_nid_no_patch() > 448u * 1024u * 1024u); + Require(before + GuestImageSegmentBytes_nid_no_patch() == configured && before > 0x80000); + } + constexpr std::size_t length = 0x80000; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 7; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelReleaseFlexibleMemory(mapped, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length / 2); + Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + length / 2, length / 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + void* oversized = nullptr; + Require(sceKernelMapFlexibleMemory(&oversized, before + PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && oversized == nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + if (mode == "limited") { + const auto length = before & ~(PS5_PAGE_SIZE - 1); + void* full = nullptr; + Require(sceKernelMapFlexibleMemory(&full, length, 3, 0) == 0); + void* extra = nullptr; + Require(sceKernelMapFlexibleMemory(&extra, PS5_PAGE_SIZE, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && extra == nullptr); + Require(sceKernelReleaseFlexibleMemory(full, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + } + if (module) { + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + Require(available > before && available + GuestImageSegmentBytes_nid_no_patch() == configured); + } + std::puts("Flexible memory budget checks passed"); +} diff --git a/core/libs/tests/GuestFlexibleMemoryModule.cpp b/core/libs/tests/GuestFlexibleMemoryModule.cpp new file mode 100644 index 0000000000..1bf220a810 --- /dev/null +++ b/core/libs/tests/GuestFlexibleMemoryModule.cpp @@ -0,0 +1,3 @@ +extern "C" { +unsigned char guestFlexibleMemoryModuleData[512u * 1024u * 1024u] = {}; +} diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index f597a80211..5312d114f1 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -15,6 +15,7 @@ struct ResourceLimit { extern "C" int APS5_VABI getrlimit_nid_postfix(int resource, ResourceLimit* limit); extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getdtablesize_nid_postfix(void); static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; @@ -124,4 +125,11 @@ int main() { }) { RequireUnsupported(resource); } + bool unsupported = false; + try { + getdtablesize_nid_postfix(); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); } diff --git a/core/libs/tests/GuestHeapRounding.cpp b/core/libs/tests/GuestHeapRounding.cpp index 1fd9527a6d..d888f7311f 100644 --- a/core/libs/tests/GuestHeapRounding.cpp +++ b/core/libs/tests/GuestHeapRounding.cpp @@ -20,6 +20,7 @@ extern "C" { void* APS5_VABI malloc_nid_postfix(std::size_t); void APS5_VABI free_nid_postfix(void*); void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -76,13 +77,14 @@ DWORD runChild(const wchar_t* mode) { int runMallocOverflow() { registerDefaultHeap(); + *__error_nid_postfix() = 0; try { auto* pointer = malloc_nid_postfix(std::numeric_limits::max() - HeaderBytes); - free_nid_postfix(pointer); - return 1; - } catch (const std::length_error& error) { - std::fprintf(stderr, "malloc overflow rejected: %s\n", error.what()); - return 0; + if (pointer != nullptr) { + free_nid_postfix(pointer); + return 1; + } + return *__error_nid_postfix() == 12 ? 0 : 4; } catch (const std::exception& error) { std::fprintf(stderr, "malloc failed with an unexpected exception: %s\n", error.what()); return 2; @@ -98,15 +100,14 @@ int runReallocOverflow() { auto* original = static_cast(malloc_nid_postfix(bytes)); std::memset(original, 0x5a, bytes); bool rejectedWithExpectedError = false; + *__error_nid_postfix() = 0; try { auto* result = realloc_nid_postfix(original, std::numeric_limits::max() - HeaderBytes); if (result != nullptr) { free_nid_postfix(result); return 4; } - } catch (const std::length_error& error) { - rejectedWithExpectedError = true; - std::fprintf(stderr, "realloc overflow rejected: %s\n", error.what()); + rejectedWithExpectedError = *__error_nid_postfix() == 12; } catch (const std::exception& error) { std::fprintf(stderr, "realloc failed with an unexpected exception: %s\n", error.what()); } catch (...) { diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index ffce322a38..d29c0021e4 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -154,7 +154,34 @@ int main() { Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); Require(result.req_id == request && result.result == static_cast(0x80436063)); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); - Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + std::uint32_t wait_timeout = 2000000; + Require(sceHttp2WaitAsync(request, &result, &wait_timeout, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + const int invalidRequests[] = {0, -1, 0x7fffffff, context, tmpl, box, doomedRequest, doomedTemplateRequest, doomedContextRequest}; + for (const int invalid : invalidRequests) { + Require(sceHttp2SendRequestAsync(invalid, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(invalid, &result, nullptr, nullptr); })); + } + const int independent = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/", 0); + Require(independent > 0 && independent != request); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr) == 0); + Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, &tag); })); + Require(sceHttp2WaitAsync(independent, &result, nullptr, nullptr) == 0); + Require(result.req_id == independent && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); + for (int i = 0; i < 2; ++i) { + Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + } + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == 0); + Require(sceHttp2DeleteRequest(independent) == 0); + Require(sceHttp2SendRequestAsync(independent, nullptr, 0, nullptr, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(independent, &result, nullptr, nullptr); })); Require(sceKernelDeleteUserEvent(eq, request) == 0); Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); })); Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); diff --git a/core/libs/tests/GuestImeOpen.cpp b/core/libs/tests/GuestImeOpen.cpp new file mode 100644 index 0000000000..15029e736b --- /dev/null +++ b/core/libs/tests/GuestImeOpen.cpp @@ -0,0 +1,350 @@ +#include "SceTypes.hpp" +#include "prx/libSceIme/include/ImeInput.hpp" +#include "prx/libSceVideoOut/include/ImeTextInput.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended); +int APS5_VABI sceImeClose_nid_postfix(void); +int APS5_VABI sceImeUpdate(EventHandler handler); +int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); +int APS5_VABI sceImeSetCaret(const Caret* caret); +int APS5_VABI sceImeGetPanelPositionAndForm(PositionAndForm* form); +void APS5_VABI sceImeParamInit(Param* param); +} + +namespace { + +constexpr int Busy = static_cast(0x80bc0001u); +constexpr int NotOpened = static_cast(0x80bc0002u); +constexpr int InvalidText = static_cast(0x80bc0006u); +constexpr int InvalidUserId = static_cast(0x80bc0010u); +constexpr int InvalidType = static_cast(0x80bc0011u); +constexpr int InvalidEnterLabel = static_cast(0x80bc0013u); +constexpr int InvalidInputMethod = static_cast(0x80bc0014u); +constexpr int InvalidOption = static_cast(0x80bc0015u); +constexpr int InvalidMaxTextLength = static_cast(0x80bc0016u); +constexpr int InvalidInputTextBuffer = static_cast(0x80bc0017u); +constexpr int InvalidPosX = static_cast(0x80bc0018u); +constexpr int InvalidPosY = static_cast(0x80bc0019u); +constexpr int InvalidHorizontalAlignment = static_cast(0x80bc001au); +constexpr int InvalidVerticalAlignment = static_cast(0x80bc001bu); +constexpr int InvalidExtended = static_cast(0x80bc001cu); +constexpr int InvalidWork = static_cast(0x80bc0020u); +constexpr int InvalidHandler = static_cast(0x80bc0022u); +constexpr int InvalidParam = static_cast(0x80bc0030u); +constexpr int InvalidAddress = static_cast(0x80bc0031u); +constexpr int InvalidReserved = static_cast(0x80bc0032u); + +constexpr uint32_t EventOpen = 0; +constexpr uint32_t EventUpdateText = 1; +constexpr uint32_t EventUpdateCaret = 2; +constexpr uint32_t EventPressClose = 4; +constexpr uint32_t EventPressEnter = 5; + +struct Recorded { + uint32_t id; + void* arg; + ImeEventParam param; + std::u16string text; +}; + +std::vector g_events; +bool g_closeOnPressClose = false; +int g_reentrantClose = 1; + +void APS5_VABI Record(void* arg, const ImeEvent* event) { + Recorded recorded{event->id, arg, event->param, {}}; + if (event->id == EventUpdateText || event->id == EventPressEnter || event->id == EventPressClose) { + recorded.text = event->param.text.str; + } + g_events.push_back(recorded); + if (g_closeOnPressClose && event->id == EventPressClose) g_reentrantClose = sceImeClose_nid_postfix(); +} + +void APS5_VABI Other(void*, const ImeEvent*) { + std::abort(); +} + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +struct Buffers { + char16_t text[2049] = {}; + alignas(16) unsigned char work[0x5000] = {}; + int arg = 0; +}; + +Param MakeParam(Buffers& buffers, uint32_t maxLength) { + Param param; + sceImeParamInit(¶m); + param.user_id = 1; + param.max_text_length = maxLength; + param.input_text_buffer = buffers.text; + param.posx = 100.0f; + param.posy = 200.0f; + param.work = buffers.work; + param.arg = &buffers.arg; + param.handler = Record; + return param; +} + +std::vector Drain() { + g_events.clear(); + Require(sceImeUpdate(Record) == 0, "update failed"); + return g_events; +} + +void Publish(ImeHostInputKind kind, const char* text = "") { + ImeHostInput input{}; + input.kind = kind; + std::strncpy(input.text, text, sizeof(input.text) - 1); + ImePublishInput_nid_postfix(input); +} + +void RequireText(const Recorded& event, uint32_t id, const std::u16string& text, uint32_t caret, uint32_t index, int32_t delta) { + Require(event.id == id, "event id"); + Require(event.text == text, "event text"); + Require(event.param.text.caret_index == caret, "event caret"); + Require(event.param.text.area_num == 1, "event area count"); + Require(event.param.text.text_area[0].mode == TextAreaMode::Edit, "event area mode"); + Require(event.param.text.text_area[0].index == index, "event edit index"); + Require(event.param.text.text_area[0].length == delta, "event edit length"); +} + +void RequireCaretSteps(const std::vector& events, uint32_t direction, std::size_t count) { + Require(events.size() == count, "caret step count"); + for (const auto& event : events) { + Require(event.id == EventUpdateCaret && event.param.caret_move == direction, "caret step"); + } +} + +void CheckValidation() { + Buffers buffers; + Require(sceImeOpen_nid_postfix(nullptr, nullptr) == InvalidAddress, "null param"); + auto expect = [&](auto change, int error, const char* message) { + Param param = MakeParam(buffers, 16); + change(param); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == error, message); + }; + expect([](Param& p) { p.user_id = -1; }, InvalidUserId, "invalid user"); + expect([](Param& p) { p.type = 5; }, InvalidType, "invalid type"); + expect([](Param& p) { p.enter_label = 4; }, InvalidEnterLabel, "invalid enter label"); + expect([](Param& p) { p.input_method = 1; }, InvalidInputMethod, "invalid input method"); + expect([](Param& p) { p.option = 0x80000000u; }, InvalidOption, "invalid option"); + expect([](Param& p) { p.max_text_length = 0; }, InvalidMaxTextLength, "zero length"); + expect([](Param& p) { p.max_text_length = 2049; }, InvalidMaxTextLength, "long length"); + expect([](Param& p) { p.input_text_buffer = nullptr; }, InvalidInputTextBuffer, "null buffer"); + expect([](Param& p) { p.posx = -1.0f; }, InvalidPosX, "negative x"); + expect([](Param& p) { p.posx = 1920.0f; }, InvalidPosX, "x past 1080p"); + expect([](Param& p) { p.posy = 1080.0f; }, InvalidPosY, "y past 1080p"); + expect([](Param& p) { p.horizontal_alignment = 3; }, InvalidHorizontalAlignment, "horizontal alignment"); + expect([](Param& p) { p.vertical_alignment = 3; }, InvalidVerticalAlignment, "vertical alignment"); + expect([](Param& p) { p.work = nullptr; }, InvalidWork, "null work"); + expect([&](Param& p) { p.work = buffers.work + 2; }, InvalidWork, "misaligned work"); + expect([](Param& p) { p.handler = nullptr; }, InvalidHandler, "null handler"); + expect([](Param& p) { p.reserved[7] = 1; }, InvalidReserved, "reserved bytes"); + buffers.text[0] = 0xdc00; + expect([](Param&) {}, InvalidText, "lone low surrogate"); + buffers.text[0] = u'\n'; + expect([](Param&) {}, InvalidText, "newline without multiline"); + buffers.text[0] = 0; + + Param param = MakeParam(buffers, 16); + ExtendedParam extended{}; + extended.priority = 4; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended priority"); + extended.priority = 0; + extended.reserved[59] = 1; + Require(sceImeOpen_nid_postfix(¶m, &extended) == InvalidExtended, "extended reserved"); + + param.posx = 3000.0f; + param.option = 0x4000; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "over-2K coordinates"); + 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"); +} + +void CheckEditing() { + Buffers buffers; + std::memcpy(buffers.text, u"ab", sizeof(u"ab")); + Param param = MakeParam(buffers, 8); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured before open"); + Require(sceImeUpdate(Record) == 0 && g_events.empty(), "update before open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "open"); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == Busy, "second open"); + Require(ImeCapturesKeyboard_nid_postfix(), "keyboard not captured"); + Require(sceImeUpdate(Other) == 0, "update with another handler"); + + auto events = Drain(); + Require(events.size() == 1 && events[0].id == EventOpen, "open event"); + Require(events[0].arg == &buffers.arg, "handler argument"); + Require(events[0].param.rect.x == 100.0f && events[0].param.rect.y == 200.0f, "open position"); + Require(events[0].param.rect.width == 793 && events[0].param.rect.height == 408, "open size"); + Require(Drain().empty(), "events delivered twice"); + + PositionAndForm form{}; + Require(sceImeGetPanelPositionAndForm(nullptr) == InvalidAddress, "null panel form"); + Require(sceImeGetPanelPositionAndForm(&form) == 0, "panel form"); + Require(form.posx == 100.0f && form.posy == 200.0f && form.width == 793 && form.height == 408, "panel form values"); + + Publish(ImeHostInputKind::Text, "c"); + events = Drain(); + Require(events.size() == 1, "one text event"); + RequireText(events[0], EventUpdateText, u"abc", 3, 2, 1); + Require(std::u16string(buffers.text) == u"abc", "input buffer"); + Require(std::u16string(reinterpret_cast(buffers.work)) == u"abc", "work copy"); + + Publish(ImeHostInputKind::Text, "\xc3\xa9\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcé\U0001F600", 6, 3, 3); + + Publish(ImeHostInputKind::Left); + RequireCaretSteps(Drain(), 1, 2); + Publish(ImeHostInputKind::Backspace); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc\U0001F600", 3, 3, -1); + Publish(ImeHostInputKind::Delete); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abc", 3, 3, -2); + Publish(ImeHostInputKind::Delete); + Require(Drain().empty(), "delete at the end"); + Publish(ImeHostInputKind::Home); + RequireCaretSteps(Drain(), 1, 3); + Publish(ImeHostInputKind::Left); + Require(Drain().empty(), "left at the start"); + Publish(ImeHostInputKind::Backspace); + Require(Drain().empty(), "backspace at the start"); + Publish(ImeHostInputKind::Right); + RequireCaretSteps(Drain(), 2, 1); + Publish(ImeHostInputKind::End); + RequireCaretSteps(Drain(), 2, 2); + + Publish(ImeHostInputKind::Text, "x\ny"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy", 5, 3, 2); + Publish(ImeHostInputKind::Text, "12\xf0\x9f\x98\x80"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12", 7, 5, 2); + Publish(ImeHostInputKind::Text, "\xf0\x9f\x98\x80"); + Require(Drain().empty(), "a pair past the limit must not be split"); + Publish(ImeHostInputKind::Text, "z"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"abcxy12z", 8, 7, 1); + Publish(ImeHostInputKind::Text, "q"); + Require(Drain().empty(), "text past the limit"); + + Require(sceImeSetText(nullptr, 1) == InvalidAddress, "null text"); + Require(sceImeSetText(u"a\nb", 3) == InvalidText, "newline in single-line text"); + Require(sceImeSetText(u"hello", 5) == 0, "set text"); + Require(std::u16string(buffers.text) == u"hello", "set text buffer"); + Require(Drain().empty(), "set text queued an event"); + Caret caret{}; + caret.index = 6; + Require(sceImeSetCaret(nullptr) == InvalidAddress, "null caret"); + Require(sceImeSetCaret(&caret) == InvalidParam, "caret past the text"); + caret.index = 1; + Require(sceImeSetCaret(&caret) == 0, "set caret"); + Publish(ImeHostInputKind::Text, "X"); + events = Drain(); + RequireText(events.at(0), EventUpdateText, u"hXello", 2, 1, 1); + + Publish(ImeHostInputKind::Enter); + events = Drain(); + RequireText(events.at(0), EventPressEnter, u"hXello", 2, 2, 0); + g_closeOnPressClose = true; + Publish(ImeHostInputKind::Escape); + Publish(ImeHostInputKind::Enter); + events = Drain(); + g_closeOnPressClose = false; + Require(events.size() == 1, "events after a close inside the handler"); + RequireText(events[0], EventPressClose, u"hXello", 2, 2, 0); + Require(g_reentrantClose == 0, "close inside the handler"); + Require(!ImeCapturesKeyboard_nid_postfix(), "keyboard captured after close"); + Require(sceImeClose_nid_postfix() == NotOpened, "second close"); + Publish(ImeHostInputKind::Text, "ignored"); + Require(std::u16string(buffers.text) == u"hXello", "input after close"); + Require(sceImeSetText(u"a", 1) == NotOpened, "set text after close"); +} + +void CheckMultiline() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + param.option = 1; + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "multiline open"); + Drain(); + Publish(ImeHostInputKind::Text, "a"); + Publish(ImeHostInputKind::Enter); + Publish(ImeHostInputKind::Text, "b\nc"); + const auto events = Drain(); + Require(events.size() == 3, "multiline events"); + RequireText(events[1], EventUpdateText, u"a\nb\nc", 2, 1, 1); + RequireText(events[2], EventUpdateText, u"a\nb\nc", 5, 2, 3); + Require(sceImeSetText(u"x\ny", 3) == 0, "multiline set text"); + Require(sceImeClose_nid_postfix() == 0, "multiline close"); +} + +void CheckHostEvents() { + Buffers buffers; + Param param = MakeParam(buffers, 16); + Require(sceImeOpen_nid_postfix(¶m, nullptr) == 0, "host events open"); + Drain(); + ImeTextInput input; + SDL_Event event{}; + event.type = SDL_TEXTINPUT; + event.text.windowID = 7; + std::strcpy(event.text.text, "hi"); + input.HandleEvent(event, 7); + event.text.windowID = 8; + input.HandleEvent(event, 7); + event = {}; + event.type = SDL_KEYDOWN; + event.key.windowID = 7; + event.key.keysym.scancode = SDL_SCANCODE_BACKSPACE; + event.key.repeat = 1; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_RETURN; + input.HandleEvent(event, 7); + event.key.repeat = 0; + input.HandleEvent(event, 7); + event.key.keysym.scancode = SDL_SCANCODE_A; + input.HandleEvent(event, 7); + event.type = SDL_KEYUP; + event.key.keysym.scancode = SDL_SCANCODE_ESCAPE; + input.HandleEvent(event, 7); + const auto events = Drain(); + Require(events.size() == 3, "host event count"); + RequireText(events[0], EventUpdateText, u"h", 2, 0, 2); + RequireText(events[1], EventUpdateText, u"h", 1, 1, -1); + Require(events[2].id == EventPressEnter, "host enter"); + Require(sceImeClose_nid_postfix() == 0, "host events close"); +} + +} + +int main() { + CheckValidation(); + CheckEditing(); + CheckMultiline(); + CheckHostEvents(); +} diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index a839d3d034..679a739b64 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -63,6 +63,8 @@ static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; 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 int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -123,6 +125,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(file, CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(file, KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); @@ -326,6 +332,10 @@ static void PipeFlagsRoundTrip() { Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + Require(ioctl_nid_postfix(pipe[0], CloseOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + Require(ioctl_nid_postfix(pipe[1], KeepOnExecRequest, nullptr) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 5f92999d29..d16ff6789a 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -465,6 +465,20 @@ int main() { Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); #endif Require(sceKernelClose(base) == 0); +#ifndef _WIN32 + if (created == 0) { + const auto throughLink = (root / "through-link.bin").string(); + Require(link_nid_postfix(symbolic.c_str(), throughLink.c_str()) == 0); + Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(throughLink))); + Require(std::filesystem::equivalent(throughLink, original) && std::filesystem::hard_link_count(original) == 2); + Require(std::filesystem::remove(throughLink) && std::filesystem::hard_link_count(original) == 1); + Require(link_nid_postfix((root / "dangling").string().c_str(), throughLink.c_str()) == -1 && Error() == 2); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix(".", directoryLink.c_str()) == 0); + Require(link_nid_postfix(directoryLink.c_str(), throughLink.c_str()) == -1 && Error() == 1); + Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); + } +#endif std::filesystem::remove_all(root); return 0; } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 78c5bd5425..ccbe1d87bf 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -400,6 +400,24 @@ static void DirectoryWatch() { Require(std::filesystem::remove_all(path) == 1); } +static void DeleteWatchedFile() { + const auto path = TemporaryName("deleted"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteWrite | NoteRename)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(unlink_nid_postfix(path.c_str()) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteDelete); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 0); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); + Require(!std::filesystem::exists(path)); +} + static void Timeout() { const int kq = kqueue_nid_postfix(); KernelEvent event{}; @@ -423,6 +441,7 @@ int main() { MoveDirectoryEntries(); #endif DirectoryWatch(); + DeleteWatchedFile(); Timeout(); return 0; } diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index c0516c6857..0aabfe21ce 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -16,6 +16,7 @@ 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_signal_nid_postfix(void** handle); } static void Require(bool value) { if (!value) std::abort(); } @@ -175,9 +176,17 @@ static void ConditionBroadcast() { _Mtx_destroy_nid_postfix(&mutex); } +static void ConditionSignal() { + void* condition = nullptr; + Require(_Cnd_init_nid_postfix(&condition) == success); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); +} + int main() { MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); + ConditionSignal(); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index cad91f320d..23c9d20d5d 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -47,6 +47,7 @@ struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_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); int APS5_VABI isnanf_nid_postfix(float); int APS5_VABI __signbitl_nid_postfix(long double); @@ -696,6 +697,24 @@ static void CheckFloatMathExports() { Require(isinf_nid_postfix(nan) == 0 && isinf_nid_postfix(1.0) == 0); } +static void CheckDoubleClassification() { + const struct { std::uint64_t bits; short code; } cases[] = { + {0x0000000000000000ull, 0}, {0x8000000000000000ull, 0}, + {0x0000000000000001ull, -2}, {0x800fffffffffffffull, -2}, + {0x0010000000000000ull, -1}, {0xbff0000000000000ull, -1}, {0x7fefffffffffffffull, -1}, + {0x7ff0000000000000ull, 1}, {0xfff0000000000000ull, 1}, + {0x7ff0000000000001ull, 2}, {0x7ff8000000000000ull, 2}, {0xfff8000000000000ull, 2}, + }; + for (const auto& test : cases) { + double value; + std::memcpy(&value, &test.bits, sizeof(value)); + if (_Dtest_nid_postfix(&value) != test.code) { + std::fprintf(stderr, "Guest _Dtest failed for %016llx\n", static_cast(test.bits)); + std::abort(); + } + } +} + static void CheckDoubleRounding(double value, std::int64_t expected, bool invalid) { for (const bool useLlround : {false, true}) { std::feclearexcept(FE_ALL_EXCEPT); @@ -718,6 +737,17 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) } } +static void CheckLongDoubleRounding(long double value, std::int64_t expected, bool invalid) { + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const std::int64_t result = llroundl_nid_postfix(value); + if (result != expected || (std::fetestexcept(FE_INVALID) != 0) != invalid || errno != 12345) { + std::fprintf(stderr, "llroundl failed for %La: expected %lld, got %lld\n", value, + static_cast(expected), static_cast(result)); + std::abort(); + } +} + static void CheckNoConversionIsPositiveZero() { for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; @@ -839,6 +869,21 @@ int main() { Require(std::abs(lgamma_nid_postfix(-0.5) - 1.2655121234846454) < 1e-15); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); + for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + CheckLongDoubleRounding(9223372036854775807.0L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.5L, INT64_MAX, false); + CheckLongDoubleRounding(9223372036854775806.0L, INT64_MAX - 1, false); + CheckLongDoubleRounding(-0x1p63L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.5L, INT64_MIN, false); + CheckLongDoubleRounding(-9223372036854775807.0L, INT64_MIN + 1, false); + for (const long double value : {9223372036854775807.5L, 0x1p63L, -9223372036854775809.0L, + std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + CheckLongDoubleRounding(value, INT64_MIN, true); + } + } + Require(std::fesetround(FE_TONEAREST) == 0); Require(logbf_nid_postfix(8.f) == 3.f && logbf_nid_postfix(-0.75f) == -1.f); Require(logbf_nid_postfix(std::numeric_limits::denorm_min()) == -149.f); Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); @@ -956,4 +1001,5 @@ int main() { CheckAtanhf(); CheckHypotSpecialValue(); CheckFmodErrno(); + CheckDoubleClassification(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 9a12142d84..d73d23368c 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -37,6 +37,7 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include +#include #endif extern "C" { @@ -909,7 +910,7 @@ static bool RegisteredGuestRange(const void* pointer, std::size_t bytes) { static void CheckGuestModuleImageProtection() { constexpr std::size_t page = 0x4000; - const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + const auto path = std::filesystem::relative(GUEST_MEMORY_ELF0_MODULE).generic_string(); void* module = dlopen_nid_postfix(path.c_str(), 2); Require(module != nullptr); auto* target = GuestModulePage(module, page); @@ -973,6 +974,45 @@ static void CheckGuestModuleImageProtection() { rejected = true; } Require(rejected); + + rejected = false; + try { + const auto libkernel = GetModuleHandleA("libkernel.prx"); + Require(libkernel != nullptr); + sceKernelMprotect(libkernel, page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + + const auto prxPath = std::filesystem::path(path).parent_path() / "guest_memory_module_foreign.prx"; + std::filesystem::copy_file(path, prxPath, std::filesystem::copy_options::overwrite_existing); + void* foreignModule = dlopen_nid_postfix(prxPath.generic_string().c_str(), 2); + Require(foreignModule != nullptr); + auto* foreignTarget = GuestModulePage(foreignModule, page); + Require(!RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 1) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READONLY); + Require(RegisteredGuestRange(foreignTarget, page)); + Require(sceKernelMprotect(foreignTarget, page, 3) == 0); + Require(ImageProtection(foreignTarget) == PAGE_READWRITE || ImageProtection(foreignTarget) == PAGE_WRITECOPY); + Require(dlclose_nid_postfix(foreignModule) == 0); + Require(!RegisteredGuestRange(foreignTarget, page)); + std::filesystem::remove(prxPath); + + const auto plainPath = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + void* plainModule = dlopen_nid_postfix(plainPath.c_str(), 2); + Require(plainModule != nullptr); + auto* plainTarget = GuestModulePage(plainModule, page); + rejected = false; + try { + sceKernelMprotect(plainTarget, page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); + Require(!RegisteredGuestRange(plainTarget, page)); + Require(dlclose_nid_postfix(plainModule) == 0); } #endif @@ -1318,6 +1358,198 @@ static void CheckHeapAfterMappingReuse() { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +#ifdef _WIN32 +static int g_accessViolations = 0; + +static LONG WINAPI CountAccessViolations(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) ++g_accessViolations; + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + +static void CheckSingleDirectMappingTracksWritesWithoutFaults() { +#ifdef _WIN32 + 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); + const auto collect = [](void* address, std::size_t bytes) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, true)); + return count; + }; + auto* bytes = static_cast(mapped); + Require(collect(mapped, page * 4) == 16); + Require(collect(mapped, page * 4) == 0); + void* const handler = AddVectoredExceptionHandler(1, CountAccessViolations); + Require(handler != nullptr); + bytes[page + 0x1000] = 7; + bytes[page * 3] = 9; + Require(RemoveVectoredExceptionHandler(handler) != 0); + Require(g_accessViolations == 0); + Require(collect(mapped, page * 4) == 2); + Require(collect(mapped, page * 4) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 2, page) == 0); + Require(bytes[page + 0x1000] == 7 && bytes[page * 3] == 9); + bytes[0] = 5; + Require(collect(mapped, page * 2) != 0); + Require(collect(mapped, page * 2) == 0); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(static_cast(second)[0x1000] == 7); + static_cast(second)[0x1001] = 11; + Require(bytes[page + 0x1001] == 11); + Require(collect(mapped, page * 2) != 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelMunmap(static_cast(mapped) + page * 3, page) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, page * 4, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 5 && data[page + 0x1000] == 7 && data[page + 0x1001] == 11 && data[page * 2] == 0 && data[page * 3] == 9); + Require(sceKernelMunmap(again, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +static void CheckSecondDirectMappingWaitsForLeases() { +#ifdef _WIN32 + 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 released{false}; + std::thread holder([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + released = true; + lease.clear(); + }); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(released); + holder.join(); + static_cast(second)[8] = 3; + Require(static_cast(mapped)[page + 8] == 3); + static_cast(mapped)[page * 2 + 8] = 4; + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelMunmap(mapped, page * 4) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 4) == 0); +#endif +} + +#ifdef _WIN32 +static void RequireResidentMapping(const void* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info)); + Require(info.State == MEM_COMMIT && info.Type == MEM_PRIVATE); +} +#endif + +static void CheckBreakingAResidentRunKeepsConcurrentAccesses() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t chunk = 0x200000; + constexpr std::size_t chunks = 4; + for (int round = 0; round < 20; ++round) { + const bool unmap = round % 2 != 0; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, chunk * chunks, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, chunk * chunks, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + auto* memory = static_cast(mapped); + const auto value = [&](std::size_t at) { return static_cast((at * 31 + static_cast(round)) | 1); }; + const auto skipped = [&](std::size_t offset) { return unmap && offset >= page && offset < page * 2; }; + std::atomic go{false}; + std::atomic done{false}; + std::atomic consistent{true}; + std::thread writer([&] { + while (!go) {} + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) memory[index * chunk + offset] = value(index * chunk + offset); + } + }); + std::thread reader([&] { + while (!go) {} + while (!done) { + for (std::size_t offset = 0; offset < chunk && !done; offset += 61) { + if (skipped(offset)) continue; + for (std::size_t index = 0; index < chunks; ++index) { + const unsigned char seen = memory[index * chunk + offset]; + if (seen != 0 && seen != value(index * chunk + offset)) consistent = false; + } + } + } + }); + std::array second{}; + go = true; + for (std::size_t index = 0; index < chunks; ++index) { + if (unmap) Require(sceKernelMunmap(static_cast(mapped) + index * chunk + page, page) == 0); + else Require(sceKernelMapDirectMemory(&second[index], page, 3, 0, phys + static_cast(index * chunk + page), 0) == 0); + } + writer.join(); + done = true; + reader.join(); + Require(consistent); + for (std::size_t index = 0; index < chunks; ++index) { + for (std::size_t offset = 0; offset < chunk; ++offset) { + if (!skipped(offset)) Require(memory[index * chunk + offset] == value(index * chunk + offset)); + } + if (unmap) continue; + for (std::size_t offset = 0; offset < page; ++offset) Require(static_cast(second[index])[offset] == value(index * chunk + page + offset)); + Require(sceKernelMunmap(second[index], page) == 0); + } + for (std::size_t index = 0; index < chunks; ++index) { + auto* base = static_cast(mapped) + index * chunk; + if (!unmap) { + Require(sceKernelMunmap(base, chunk) == 0); + continue; + } + Require(sceKernelMunmap(base, page) == 0); + Require(sceKernelMunmap(base + page * 2, chunk - page * 2) == 0); + } + Require(sceKernelReleaseDirectMemory(phys, chunk * chunks) == 0); + } +#endif +} + +static void CheckRemappedDirectMemoryIsChargedOnce() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x10000000; + const auto committed = [] { + PERFORMANCE_INFORMATION info{}; + info.cb = sizeof(info); + Require(K32GetPerformanceInfo(&info, sizeof(info)) != 0); + return static_cast(info.CommitTotal) * static_cast(info.PageSize); + }; + bool once = false; + for (int attempt = 0; attempt < 3 && !once; ++attempt) { + const auto before = committed(); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, phys, 0) == 0); + RequireResidentMapping(mapped); + std::memset(mapped, 0xa5, bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + void* again = nullptr; + Require(sceKernelMapDirectMemory(&again, bytes, 3, 0, phys, 0) == 0); + const auto* data = static_cast(again); + Require(data[0] == 0xa5 && data[bytes / 2] == 0xa5 && data[bytes - 1] == 0xa5); + once = committed() - before < static_cast(bytes + bytes / 2); + Require(sceKernelMunmap(again, bytes) == 0); + Require(sceKernelReleaseDirectMemory(phys, bytes) == 0); + } + Require(once); +#endif +} + static void CheckSharedWriteTracking() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -1492,13 +1724,52 @@ static void CheckFailedCollectKeepsWrites() { #endif } +static void CheckSplitPrivateCollect() { +#ifdef _WIN32 + constexpr std::size_t granule = 0x4000; + constexpr std::size_t regions = 8; + constexpr std::size_t bytes = granule * regions; + constexpr std::size_t pages = bytes / 4096; + auto* block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, granule)); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, granule); + const auto address = reinterpret_cast(block); + const auto collect = [&](std::size_t capacity) { + std::vector found; + std::array buffer{}; + for (auto cursor = address; cursor < address + bytes;) { + std::size_t count = capacity; + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, address + bytes - cursor, buffer.data(), &count, true)); + for (std::size_t i = 0; i < count; ++i) found.push_back(reinterpret_cast(buffer[i])); + if (count < capacity) break; + cursor = found.back() + 4096; + } + return found; + }; + collect(pages); + for (const std::size_t capacity : {pages, std::size_t{5}, std::size_t{1}}) { + for (std::size_t page = 0; page < pages; ++page) static_cast(block)[page * 4096 + 7] = static_cast(capacity + page); + const auto found = collect(capacity); + Require(found.size() == pages); + for (std::size_t page = 0; page < pages; ++page) Require(found[page] == address + page * 4096); + Require(collect(pages).empty()); + } + static_cast(block)[granule * 3 + 4096 + 1] = 9; + const auto one = collect(pages); + Require(one.size() == 1 && one[0] == address + granule * 3 + 4096); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#endif +} + static void CheckPinnedSharedPages() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); void* mapped = nullptr; + void* alias = nullptr; Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&alias, page * 3, 3, 0, phys, 0) == 0); auto* bytes = static_cast(mapped); const auto collect = [&] { std::array pages{}; @@ -1530,6 +1801,7 @@ static void CheckPinnedSharedPages() { Require(protection(page) == PAGE_READONLY); bytes[page + 8] = 11; Require(collect() == 1); + Require(sceKernelMunmap(alias, page * 3) == 0); Require(sceKernelMunmap(mapped, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); #endif @@ -2156,6 +2428,7 @@ int main() { CheckFixedMappingReplacesPartialOverlap(); CheckFixedMappingOverHoleInsideAllocation(); CheckDirectMemoryGpuProtBits(); + CheckSplitPrivateCollect(); CheckGpuAccessFollowsProtection(); CheckGpuMapsAreObserved(); CheckClearingTheObserverWaitsForItsCalls(); @@ -2191,6 +2464,10 @@ int main() { CheckReadsIntoSharedWriteTracking(); CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); + CheckSingleDirectMappingTracksWritesWithoutFaults(); + CheckSecondDirectMappingWaitsForLeases(); + CheckBreakingAResidentRunKeepsConcurrentAccesses(); + CheckRemappedDirectMemoryIsChargedOnce(); #if defined(__linux__) CheckWriteWatch(); CheckWriteWatchArmedCollect(); diff --git a/core/libs/tests/GuestMemoryModule.S b/core/libs/tests/GuestMemoryModule.S new file mode 100644 index 0000000000..a16839b3a9 --- /dev/null +++ b/core/libs/tests/GuestMemoryModule.S @@ -0,0 +1,3 @@ +.section .elf0,"dr" +.p2align 4 +.long 0 diff --git a/core/libs/tests/GuestMutexContention.cpp b/core/libs/tests/GuestMutexContention.cpp new file mode 100644 index 0000000000..dbb0da5e2f --- /dev/null +++ b/core/libs/tests/GuestMutexContention.cpp @@ -0,0 +1,200 @@ +#include "SceTypes.hpp" +#include +#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 scePthreadMutexattrInit(PthreadMutexattr*); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr*, int); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr*); +int APS5_VABI scePthreadMutexInit(PthreadMutex*, const PthreadMutexattr*, const char*); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex*); +int APS5_VABI scePthreadMutexLock(PthreadMutex*); +int APS5_VABI scePthreadMutexTimedlock(PthreadMutex*, KernelUseconds); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex*); +} + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr int TimedOut = static_cast(0x8002003cu); + +void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +std::uint64_t ThreadCycles() { + ULONG64 cycles = 0; + Require(QueryThreadCycleTime(GetCurrentThread(), &cycles) != 0, "QueryThreadCycleTime failed"); + return cycles; +} + +std::uint64_t BusyCycles(int milliseconds) { + const auto before = ThreadCycles(); + const auto until = Clock::now() + std::chrono::milliseconds(milliseconds); + while (Clock::now() < until) YieldProcessor(); + return ThreadCycles() - before; +} + +struct Handoff { + PthreadMutex mutex = nullptr; + bool timed = false; + bool destroy = false; + std::atomic waiting{false}; + std::atomic released{0}; + std::int64_t latencyNanos = 0; + std::uint64_t waitCycles = 0; +}; + +struct HandoffResult { + std::int64_t latencyNanos; + std::uint64_t waitCycles; +}; + +void* APS5_VABI HandoffWorker(void* opaque) { + auto& context = *static_cast(opaque); + const auto cyclesBefore = ThreadCycles(); + context.waiting.store(true); + Require((context.timed ? scePthreadMutexTimedlock(&context.mutex, 5000000) : scePthreadMutexLock(&context.mutex)) == 0, "the waiter did not acquire the mutex"); + const auto acquired = Clock::now().time_since_epoch().count(); + context.waitCycles = ThreadCycles() - cyclesBefore; + context.latencyNanos = acquired - context.released.load(); + Require(scePthreadMutexUnlock(&context.mutex) == 0, "the waiter could not unlock"); + if (context.destroy) Require(scePthreadMutexDestroy(&context.mutex) == 0, "the waiter could not destroy the mutex"); + return nullptr; +} + +HandoffResult RunHandoff(PthreadMutex mutex, bool timed, bool destroy, int holdMilliseconds, int extraMicroseconds = 0) { + Handoff context; + context.mutex = mutex; + context.timed = timed; + context.destroy = destroy; + PthreadMutex holder = mutex; + Require(scePthreadMutexLock(&holder) == 0, "the holder could not lock"); + Pthread worker = nullptr; + Require(scePthreadCreate(&worker, nullptr, HandoffWorker, &context, "mutex waiter") == 0, "cannot create the waiter"); + while (!context.waiting.load()) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(holdMilliseconds)); + const auto extra = Clock::now() + std::chrono::microseconds(extraMicroseconds); + while (Clock::now() < extra) YieldProcessor(); + context.released.store(Clock::now().time_since_epoch().count()); + Require(scePthreadMutexUnlock(&holder) == 0, "the holder could not unlock"); + Require(scePthreadJoin(worker, nullptr) == 0, "cannot join the waiter"); + return {context.latencyNanos, context.waitCycles}; +} + +PthreadMutex CreateMutex(bool recursive) { + PthreadMutexattr attributes = nullptr; + Require(scePthreadMutexattrInit(&attributes) == 0 && scePthreadMutexattrSettype(&attributes, recursive ? 2 : 1) == 0, "cannot prepare mutex attributes"); + PthreadMutex mutex = nullptr; + Require(scePthreadMutexInit(&mutex, &attributes, nullptr) == 0, "cannot create the mutex"); + Require(scePthreadMutexattrDestroy(&attributes) == 0, "cannot destroy mutex attributes"); + return mutex; +} + +struct Contention { + PthreadMutex mutex = nullptr; + std::uint64_t rounds = 0; + std::atomic start{false}; + std::atomic stop{false}; + std::uint64_t shared = 0; + std::array acquired{}; + std::atomic next{0}; +}; + +void* APS5_VABI ContentionWorker(void* opaque) { + auto& context = *static_cast(opaque); + const int index = context.next.fetch_add(1); + PthreadMutex mutex = context.mutex; + while (!context.start.load()) std::this_thread::yield(); + std::uint64_t count = 0; + while (context.rounds ? count < context.rounds : !context.stop.load(std::memory_order_relaxed)) { + Require(scePthreadMutexLock(&mutex) == 0, "contended lock failed"); + const std::uint64_t seen = context.shared; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + context.shared = seen + 1; + Require(scePthreadMutexUnlock(&mutex) == 0, "contended unlock failed"); + ++count; + for (int spin = 0; spin < 64; ++spin) YieldProcessor(); + } + context.acquired[index] = count; + return nullptr; +} + +void* APS5_VABI TimeoutWorker(void* opaque) { + PthreadMutex mutex = *static_cast(opaque); + const auto started = Clock::now(); + Require(scePthreadMutexTimedlock(&mutex, 20000) == TimedOut, "a timed lock of a held mutex did not time out"); + Require(Clock::now() - started >= std::chrono::milliseconds(20), "a timed lock of a held mutex timed out before its deadline"); + return nullptr; +} + +} + +int main(int argc, char** argv) { + const bool report = argc > 1 && std::strcmp(argv[1], "--report") == 0; + const std::uint64_t busyCycles = report ? BusyCycles(20) * 10 : 0; + for (const bool recursive : {false, true}) { + PthreadMutex mutex = CreateMutex(recursive); + for (const bool timed : {false, true}) { + if (!report) { + for (int round = 0; round < 4; ++round) RunHandoff(mutex, timed, false, 3); + continue; + } + std::vector latencies; + for (int round = 0; round < 31; ++round) latencies.push_back(RunHandoff(mutex, timed, false, 3, round * 617 % 1000).latencyNanos); + std::sort(latencies.begin(), latencies.end()); + const auto blocked = RunHandoff(mutex, timed, false, 200); + const double share = 100.0 * static_cast(blocked.waitCycles) / static_cast(busyCycles); + std::printf("%s %s: handoff median %.1f us, p90 %.1f us, max %.1f us; a waiter blocked for 200 ms used %.1f %% of one processor\n", + recursive ? "recursive" : "errorcheck", timed ? "timed lock" : "lock", + latencies[15] / 1000.0, latencies[27] / 1000.0, latencies[30] / 1000.0, share); + } + PthreadMutex held = mutex; + Require(scePthreadMutexLock(&held) == 0, "cannot hold the mutex for the timeout"); + Pthread timeout = nullptr; + Require(scePthreadCreate(&timeout, nullptr, TimeoutWorker, &mutex, "mutex timeout") == 0, "cannot create the timeout waiter"); + Require(scePthreadJoin(timeout, nullptr) == 0, "cannot join the timeout waiter"); + Require(scePthreadMutexUnlock(&held) == 0, "cannot release the mutex after the timeout"); + Contention context; + context.mutex = mutex; + context.rounds = report ? 0 : 20000; + std::array workers{}; + for (auto& worker : workers) Require(scePthreadCreate(&worker, nullptr, ContentionWorker, &context, "mutex contender") == 0, "cannot create a contender"); + const auto started = Clock::now(); + context.start.store(true); + if (report) { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + context.stop.store(true); + } + for (auto& worker : workers) Require(scePthreadJoin(worker, nullptr) == 0, "cannot join a contender"); + const double seconds = std::chrono::duration(Clock::now() - started).count(); + std::uint64_t total = 0; + for (const auto count : context.acquired) total += count; + Require(total == context.shared, "the mutex did not exclude its contenders"); + const auto fewest = *std::min_element(context.acquired.begin(), context.acquired.end()); + if (report) { + std::printf("%s: four contenders made %.0f acquisitions per second, the slowest %.0f per second\n", + recursive ? "recursive" : "errorcheck", total / seconds, fewest / seconds); + } else { + Require(fewest == context.rounds, "a contender did not finish its acquisitions"); + } + Require(scePthreadMutexDestroy(&mutex) == 0, "cannot destroy the mutex"); + for (int round = 0; round < 200; ++round) RunHandoff(CreateMutex(recursive), round % 2 != 0, true, 0); + } + std::puts("guest mutex contention: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp index f99ceafb24..92cdf6c335 100644 --- a/core/libs/tests/GuestNpAuth.cpp +++ b/core/libs/tests/GuestNpAuth.cpp @@ -27,6 +27,7 @@ int main() { constexpr int aborted = static_cast(0x80550304); constexpr int requestNotFound = static_cast(0x80550306); constexpr int invalidId = static_cast(0x80550307); + constexpr int pollRunning = 1; const int syncReq = sceNpAuthCreateRequest(); const int syncReq2 = sceNpAuthCreateRequest(); @@ -64,6 +65,10 @@ int main() { Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); int asyncPoll = 0; + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == pollRunning); + Require(asyncPoll == 0); + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); + Require(asyncPoll == signedOut); Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); Require(asyncPoll == signedOut); int asyncWait = 0; diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index 3f31d7d99f..b01b7e1902 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -17,6 +17,10 @@ int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); +int APS5_VABI sceNpWebApi2CreateUserContext(int, int); +int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); +int APS5_VABI sceNpWebApi2SetRequestTimeout(std::int64_t, std::uint32_t); +int APS5_VABI sceNpWebApi2DeleteRequest(std::int64_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -43,6 +47,11 @@ int main() { const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); + const int user = sceNpWebApi2CreateUserContext(context, 1); + std::int64_t request = 0; + Require(user > 0 && sceNpWebApi2CreateRequest(user, "sessionManager", "/v1/sessions", "GET", nullptr, &request) == 0); + Require(sceNpWebApi2SetRequestTimeout(request, 30000000) == 0); + Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index ef1bf7304d..0b462ff31c 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -40,6 +40,12 @@ const std::uint8_t OPUS_STEREO[] = { 0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5, }; +const std::uint8_t OPUS_NARROWBAND[] = { + 11, 0, 0x08, 0x83, 0x9C, 0xE1, 0x7B, 0x2F, 0x52, 0xCA, 0x9E, 0x75, 0xD8, + 13, 0, 0x08, 0x96, 0x51, 0x3A, 0x33, 0x4A, 0xF8, 0x29, 0xF5, 0xBE, 0x40, 0x15, 0x98, + 10, 0, 0x08, 0x85, 0x89, 0x9D, 0x73, 0x0F, 0xC2, 0xB1, 0xE2, 0x48, +}; + } // namespace int main() { @@ -111,4 +117,25 @@ int main() { Require(sceOpusDecDecode(otherState.data(), OPUS_STEREO + offset + 2, OPUS_STEREO[offset], reference.data(), 3840) == 3840); Require(sceOpusDecDestroy(otherState.data()) == 0); Require(sceOpusDecTerminate(&otherContext) == 0); + + std::uint32_t narrowContext = 0; + std::array narrowState{}; + Require(sceOpusDecInitialize(&narrowContext) == 0); + Require(sceOpusDecCreateEx(&narrowContext, narrowState.data(), 48000, 1) == 0); + std::array narrow{}; + bool audible = false; + for (std::size_t at = 0; at < sizeof(OPUS_NARROWBAND);) { + const int bytes = OPUS_NARROWBAND[at] | (OPUS_NARROWBAND[at + 1] << 8); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_NARROWBAND + at + 2, bytes, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + audible = audible || std::any_of(narrow.begin() + 1, narrow.end() - 1, [](auto value) { return value != 0 && value != 12345; }); + at += bytes + 2; + } + Require(audible); + narrow.fill(12345); + Require(sceOpusDecDecode(narrowState.data(), OPUS_STEREO + 2, 70, narrow.data() + 1, 1920) == 1920); + Require(narrow.front() == 12345 && narrow.back() == 12345); + Require(sceOpusDecDestroy(narrowState.data()) == 0); + Require(sceOpusDecTerminate(&narrowContext) == 0); } diff --git a/core/libs/tests/GuestPageTableStats.cpp b/core/libs/tests/GuestPageTableStats.cpp new file mode 100644 index 0000000000..6a47951c8a --- /dev/null +++ b/core/libs/tests/GuestPageTableStats.cpp @@ -0,0 +1,138 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelGetPageTableStats(int*, int*, int*, int*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Page table stats check failed at %s:%u\n", location.file_name(), static_cast(location.line())); + std::abort(); + } +} + +constexpr std::size_t PageTableGranule = 0x200000; +constexpr int PageTableTotal = 2048; + +struct Stats { + int cpuTotal; + int cpuAvailable; + int gpuTotal; + int gpuAvailable; +}; + +static Stats Query() { + Stats stats{}; + Require(sceKernelGetPageTableStats(&stats.cpuTotal, &stats.cpuAvailable, &stats.gpuTotal, &stats.gpuAvailable) == 0); + return stats; +} + +static int Granules(const void* addr, std::size_t len) { + const auto start = reinterpret_cast(addr); + return static_cast((start + len - 1) / PageTableGranule - start / PageTableGranule + 1); +} + +static void CheckRejectedArguments() { + int value = 0; + Require(sceKernelGetPageTableStats(nullptr, &value, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, nullptr, &value, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, nullptr, &value) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&value, &value, &value, nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void CheckFlexibleMapping() { + const auto before = Query(); + Require(before.cpuTotal == PageTableTotal && before.gpuTotal == PageTableTotal); + Require(before.cpuAvailable <= before.cpuTotal && before.gpuAvailable <= before.gpuTotal); + + constexpr std::size_t len = 3 * PageTableGranule; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, len, 3, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(during.gpuAvailable == before.gpuAvailable); + + Require(sceKernelMprotect(mapped, len, 0x30) == 0); + during = Query(); + Require(during.cpuAvailable == before.cpuAvailable); + Require(during.gpuAvailable == before.gpuAvailable - entries); + + Require(sceKernelMunmap(mapped, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckDualAccessMapping() { + const auto before = Query(); + constexpr std::size_t len = PageTableGranule; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, len, 0x33, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + const auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(during.gpuAvailable == before.gpuAvailable - entries); + Require(sceKernelMunmap(mapped, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckSharedGranule() { + const auto before = Query(); + constexpr std::size_t page = 0x4000; + void* span = nullptr; + Require(sceKernelReserveVirtualRange(&span, 2 * PageTableGranule, 0, PageTableGranule) == 0 && span != nullptr); + const auto base = reinterpret_cast(span); + const auto aligned = base % PageTableGranule == 0 ? base : (base / PageTableGranule + 1) * PageTableGranule; + void* first = reinterpret_cast(aligned); + void* second = reinterpret_cast(aligned + page); + Require(sceKernelMapFlexibleMemory(&first, page, 3, 0) == 0 && first == reinterpret_cast(aligned)); + Require(sceKernelMapFlexibleMemory(&second, page, 3, 0) == 0 && second == reinterpret_cast(aligned + page)); + const auto during = Query(); + Require(during.cpuAvailable == before.cpuAvailable - 2); + Require(during.gpuAvailable == before.gpuAvailable); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +static void CheckGpuDirectMapping() { + const auto before = Query(); + constexpr std::size_t len = 2 * PageTableGranule; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, len, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, len, 0x33, 0, phys, 0) == 0 && mapped != nullptr); + const auto entries = Granules(mapped, len); + const auto during = Query(); + Require(during.gpuAvailable == before.gpuAvailable - entries); + Require(during.cpuAvailable == before.cpuAvailable - entries); + Require(sceKernelMunmap(mapped, len) == 0); + Require(sceKernelReleaseDirectMemory(phys, len) == 0); + const auto after = Query(); + Require(after.cpuAvailable == before.cpuAvailable && after.gpuAvailable == before.gpuAvailable); +} + +int main() { + CheckRejectedArguments(); + CheckFlexibleMapping(); + CheckDualAccessMapping(); + CheckSharedGranule(); + CheckGpuDirectMapping(); + return 0; +} diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 2cea5041e1..cf93ef9eb2 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); @@ -39,31 +42,30 @@ static int CheckPackageChunkTable(const char* root) { return 0; } -static void RunWithPackageChunkTable(const char* self) { - const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); - std::filesystem::remove_all(root); - std::filesystem::create_directories(root / "app0" / "sce_sys"); - char header[0x100] = {'p', 'l', 'g', 'x'}; - header[0x0A] = 3; - std::ofstream(root / "app0" / "sce_sys" / "playgo-chunk.dat", std::ios::binary).write(header, sizeof(header)); - const std::string arguments = "\"" + std::string(self) + "\" --package-chunk-table \"" + root.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - const int status = std::system(command.c_str()); - std::filesystem::remove_all(root); - Require(status == 0); +static int CheckScenarioWithoutChunkTable(const char* root) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + std::int8_t loci[3] = {0, 0, 0}; + Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoClose(handle) == 0); + return 0; } -int main(int argc, char** argv) { - if (argc == 3 && std::string_view(argv[1]) == "--package-chunk-table") return CheckPackageChunkTable(argv[2]); - RunWithPackageChunkTable(argv[0]); +static int CheckNoPlayGoFile(const char* root) { + std::filesystem::current_path(root); constexpr int badHandle = static_cast(0x80B20009); constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); constexpr int badLocus = static_cast(0x80B20010); + constexpr int badChunkId = static_cast(0x80B2000C); PlayGoInitParams init{}; int handle = 0; @@ -84,8 +86,10 @@ int main(int argc, char** argv) { Require(scePlayGoSetToDoList(handle, todo, 2) == 0); todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); + todo[1] = {1, 3, 0}; + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); todo[1] = {0xFFFF, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); std::uint32_t entries = 1; Require(scePlayGoGetToDoList(handle, todo, 2, &entries) == 0 && entries == 0); @@ -95,9 +99,73 @@ int main(int argc, char** argv) { Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; - Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == badChunkId); + const std::uint16_t onlyDeclared[2] = {0, 0}; + Require(scePlayGoGetLocus(handle, onlyDeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); Require(scePlayGoClose(handle + 1) == badHandle); Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckRejectedLayout(const char* root, const char* fileName) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t chunk = 0; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &chunk, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find(fileName) != std::string_view::npos; + } + Require(threw); + return 0; +} + +static void RunLayout(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) std::ofstream(root / "app0" / "sce_sys" / name, std::ios::binary).write(content.data(), static_cast(content.size())); + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +static std::string ChunkTableFile() { + std::string header(0x100, '\0'); + header.replace(0, 4, "plgx"); + header[0x0A] = 3; + return header; +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-file") return CheckNoPlayGoFile(argv[2]); + if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); + if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); + if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); + if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); + if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); + } + RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); + 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"}}); + return 0; } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 37171623de..1738f11dd8 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -210,6 +210,10 @@ int main() { PollDescriptor closed[2]{{client, In, 0}, {listener, In, 0}}; Require(poll_nid_postfix(closed, 2, -1) == 1 && closed[0].revents == Invalid && closed[1].revents == 0); Require(poll_nid_postfix(nullptr, 0, 10) == 0); + const auto shortWaitStart = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(nullptr, 0, 1) == 0); + const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200)); Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); TestPipes(listener); diff --git a/core/libs/tests/GuestPositionedRace.cpp b/core/libs/tests/GuestPositionedRace.cpp new file mode 100644 index 0000000000..6c936c56b4 --- /dev/null +++ b/core/libs/tests/GuestPositionedRace.cpp @@ -0,0 +1,77 @@ +#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 +#include +#include +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelPread(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Positioned race check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +int main() { + const auto root = std::filesystem::path("anyps5-positioned-race-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "blocks.bin"; + constexpr std::int64_t kBlocks = 8; + constexpr std::size_t kBlockSize = 4096; + { + std::ofstream stream(file, std::ios::binary); + Require(static_cast(stream)); + for (std::int64_t block = 0; block < kBlocks; ++block) { + stream << std::string(kBlockSize, static_cast('A' + block)); + } + } + const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + std::array expected{}; + expected.fill('A'); + std::atomic stop{false}; + std::atomic misread{false}; + std::array seeked{}; + seeked.fill('H'); + std::thread seeker([&] { + std::array sink{}; + while (!stop.load()) { + if (sceKernelLseek(fd, (kBlocks - 1) * kBlockSize, 0) != (kBlocks - 1) * kBlockSize) { + misread = true; + return; + } + if (sceKernelRead(fd, sink.data(), sink.size()) != static_cast(sink.size()) || + std::memcmp(sink.data(), seeked.data(), seeked.size()) != 0) { + misread = true; + return; + } + } + }); + for (int i = 0; i < 5000 && !misread.load(); ++i) { + std::array bytes{}; + const auto got = sceKernelPread(fd, bytes.data(), bytes.size(), 0); + if (got != static_cast(bytes.size()) || std::memcmp(bytes.data(), expected.data(), expected.size()) != 0) { + misread = true; + } + } + stop = true; + seeker.join(); + Require(!misread); + Require(sceKernelClose(fd) == 0); + Require(std::filesystem::remove_all(root) > 0); +} diff --git a/core/libs/tests/GuestPthreadAtfork.cpp b/core/libs/tests/GuestPthreadAtfork.cpp new file mode 100644 index 0000000000..0cf6b91fec --- /dev/null +++ b/core/libs/tests/GuestPthreadAtfork.cpp @@ -0,0 +1,48 @@ +#include "prx/libkernel/Pthread/include/Atfork.hpp" +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}; + +static void APS5_VABI Prepare() { ++calls; } +static void APS5_VABI Parent() { ++calls; } +static void APS5_VABI Child() { ++calls; } + +static bool Same(const PthreadAtforkHandlers& entry, PthreadAtforkHandler prepare, PthreadAtforkHandler parent, PthreadAtforkHandler child) { + return entry.prepare == prepare && entry.parent == parent && entry.child == child; +} + +int main() { + Require(PthreadAtforkHandlers_nid_no_patch().empty()); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + Require(pthread_atfork_nid_postfix(nullptr, Parent, nullptr) == 0); + Require(pthread_atfork_nid_postfix(nullptr, nullptr, nullptr) == 0); + Require(pthread_atfork_nid_postfix(Prepare, Parent, Child) == 0); + auto handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4); + Require(Same(handlers[0], Prepare, Parent, Child)); + Require(Same(handlers[1], nullptr, Parent, nullptr)); + Require(Same(handlers[2], nullptr, nullptr, nullptr)); + Require(Same(handlers[3], Prepare, Parent, Child)); + + constexpr std::size_t threadCount = 4; + constexpr std::size_t perThread = 64; + std::vector threads; + for (std::size_t index = 0; index < threadCount; ++index) { + threads.emplace_back([] { + for (std::size_t count = 0; count < perThread; ++count) Require(pthread_atfork_nid_postfix(nullptr, nullptr, Child) == 0); + }); + } + for (auto& thread : threads) thread.join(); + handlers = PthreadAtforkHandlers_nid_no_patch(); + Require(handlers.size() == 4 + threadCount * perThread); + Require(Same(handlers[3], Prepare, Parent, Child)); + for (std::size_t index = 4; index < handlers.size(); ++index) Require(Same(handlers[index], nullptr, nullptr, Child)); + Require(calls == 0); + return 0; +} diff --git a/core/libs/tests/GuestPthreadStack.cpp b/core/libs/tests/GuestPthreadStack.cpp index 21045925e5..5fcbe7ac52 100644 --- a/core/libs/tests/GuestPthreadStack.cpp +++ b/core/libs/tests/GuestPthreadStack.cpp @@ -1,4 +1,14 @@ #include "SceTypes.hpp" +#ifdef __linux__ +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#endif #include #include #include @@ -12,6 +22,9 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackAddress, std::size_t* stackSize); Pthread APS5_VABI scePthreadSelf(); +#ifdef __linux__ +int APS5_VABI scePthreadDetach(Pthread thread); +#endif } static constexpr int SCE_OK = 0; @@ -64,7 +77,41 @@ static void* APS5_VABI Worker(void* arg) { return arg; } +#ifdef __linux__ +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end); +static std::atomic observedUnmaps{0}; +static std::atomic retiredStackWord{0}; +static std::atomic retiredUnmapped{false}; +static std::atomic detachedDone{false}; +static int detachedTid = 0; + +static void* APS5_VABI DetachedWorker(void*) { + int local = 0; + detachedTid = static_cast(syscall(SYS_gettid)); + retiredStackWord.store(reinterpret_cast(&local), std::memory_order_relaxed); + detachedDone.store(true, std::memory_order_release); + return nullptr; +} + +static void* APS5_VABI ReaperWorker(void*) { + return nullptr; +} + +static void ObserveUnmap(std::uintptr_t address, std::size_t bytes) { + if (GuestThreadStackHostRangeOverlaps(address, address + bytes)) { + std::fprintf(stderr, "Retired stack still owns host-only pages after unmap\n"); + std::abort(); + } + observedUnmaps.fetch_add(1, std::memory_order_relaxed); + const auto retired = retiredStackWord.load(std::memory_order_relaxed); + if (retired >= address && retired - address < bytes) retiredUnmapped.store(true, std::memory_order_relaxed); +} +#endif + int main() { +#ifdef __linux__ + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(&ObserveUnmap); +#endif for (auto stackSize : STACK_SIZES) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); @@ -76,4 +123,25 @@ int main() { Require(scePthreadJoin(thread, &result) == SCE_OK); Require(result == &stackSize); } +#ifdef __linux__ + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, nullptr, DetachedWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadDetach(detached) == SCE_OK); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!detachedDone.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + const auto task = "/proc/self/task/" + std::to_string(detachedTid); + while (access(task.c_str(), F_OK) == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Pthread reaper = nullptr; + Require(scePthreadCreate(&reaper, nullptr, ReaperWorker, nullptr, nullptr) == SCE_OK); + Require(scePthreadJoin(reaper, nullptr) == SCE_OK); + Require(retiredUnmapped.load(std::memory_order_relaxed)); + GuestAllocations::GuestAllocationsSetUnmapObserver_nid_postfix(nullptr); + Require(observedUnmaps.load(std::memory_order_relaxed) != 0); +#endif } diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index bb66c3979c..fded34f311 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include extern "C" { void* APS5_VABI reallocalign_nid_postfix(void*, std::size_t, std::size_t); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -14,13 +16,6 @@ void Require(bool value) { if (!value) throw std::runtime_error("reallocalign check failed"); } -template -void Reject(TAction action) { - bool rejected = false; - try { action(); } catch (const std::exception&) { rejected = true; } - Require(rejected); -} - } int main() { @@ -35,7 +30,12 @@ int main() { auto* shrunk = static_cast(reallocalign_nid_postfix(grown, 16, 16)); Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 16) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 0); }); - Reject([&] { reallocalign_nid_postfix(shrunk, 16, 3); }); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 0) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, 16, 3) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(reallocalign_nid_postfix(shrunk, std::numeric_limits::max(), 16) == nullptr && *__error_nid_postfix() == 12); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); Require(reallocalign_nid_postfix(shrunk, 0, 16) == nullptr); } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index c51a1cb5c1..9249ab3838 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -18,6 +18,8 @@ namespace { constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; +constexpr int SaveDataErrorBusy = -2137063421; +constexpr int SaveDataErrorNotFound = -2137063416; int failures = 0; @@ -41,6 +43,26 @@ void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedS Check(sceSaveDataUmount2(0, &result.mount_point) == 0, what + ": unmounts"); } +void CheckFailedMountKeepsResult() { + SceSaveDataDirName held{}; + std::memcpy(held.data, "held", sizeof("held")); + SaveDataMount3 mount{}; + mount.dir_name = &held; + mount.mount_mode = MountReadWrite | MountCreate2; + SaveDataMountResult result{}; + Check(sceSaveDataMount3(&mount, &result) == 0, "first mount of a save"); + const SaveDataMountResult mounted = result; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorBusy, "second mount of a mounted save is BUSY"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a BUSY mount leaves the result as it was"); + SceSaveDataDirName absent{}; + std::memcpy(absent.data, "absent", sizeof("absent")); + mount.dir_name = &absent; + mount.mount_mode = MountReadWrite; + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorNotFound, "open of a missing save is NOT_FOUND"); + Check(std::memcmp(&result, &mounted, sizeof(result)) == 0, "a NOT_FOUND mount leaves the result as it was"); + Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save"); +} + } int main() { @@ -53,6 +75,7 @@ int main() { MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save"); MountAndCheck("made", MountReadWrite, 0u, "open of an existing save"); + CheckFailedMountKeepsResult(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 53d0cc5066..6cea92d921 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -1,10 +1,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include #include #include #include @@ -16,6 +18,7 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDa int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); } static constexpr int SaveDataErrorParameter = -2137063424; @@ -123,6 +126,42 @@ static void TrailingWildcards() { Require(SearchPattern("TRAIL_%") == Names{"TRAIL1"}, "_ must still match exactly one character"); } +static std::uint32_t SearchTitle(const char* titleId, bool second) { + SceSaveDataTitleId title{}; + std::snprintf(title.data, sizeof(title.data), "%s", titleId != nullptr ? titleId : ""); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "OTHER_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.title_id = titleId != nullptr ? &title : nullptr; + cond.dir_name = &pattern; + std::array names{}; + names[0].data[0] = '!'; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 1; + result.hit_num = 7; + result.set_num = 7; + Require((second ? sceSaveDataDirNameSearch2(&cond, &result) : sceSaveDataDirNameSearch(&cond, &result)) == 0, "title search failed"); + Require(result.set_num == std::min(result.hit_num, 1), "title search must set the names it found"); + Require(result.set_num == 1 ? std::string(names[0].data) == "OTHER_A" : names[0].data[0] == '!', "title search returned the wrong name"); + return result.hit_num; +} + +static void OtherTitle() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs("{\"titleId\":\"PPSA00000\",\"localizedParameters\":{\"en-US\":{\"titleName\":\"Example\"}}}", param); + std::fclose(param); + std::filesystem::create_directories(std::filesystem::path("_sd") / "OTHER_A"); + for (const bool second : {false, true}) { + Require(SearchTitle(nullptr, second) == 1, "a search without a title id must find the running title's save"); + Require(SearchTitle("PPSA00000", second) == 1, "a search for the running title must find its save"); + Require(SearchTitle("PPSA17221", second) == 0, "a search for another title must find none of the running title's saves"); + } +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -171,6 +210,68 @@ static void SearchAndMount() { } } +static void CreateSized(const char* name, std::uint64_t blocks, std::size_t bytes) { + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.blocks = blocks; + mount.mount_mode = 2 | 4; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "creating a sized save failed"); + if (bytes != 0) { + std::ofstream file(std::filesystem::path("_sd") / name / "data", std::ios::binary); + file << std::string(bytes, 'x'); + } + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); +} + +static std::vector SizeSearch(std::uint32_t key, std::vector& found) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SIZE_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + std::array names{}; + std::array infos{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = 2; + result.infos = infos.data(); + Require(sceSaveDataDirNameSearch(&cond, &result) == 0 && result.set_num == 2, "the sized search must return both saves"); + found.clear(); + for (const auto& name : names) found.emplace_back(name.data); + 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"); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "SIZE_B"); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 1; + SaveDataMountResult mounted{}; + 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"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / @@ -181,7 +282,9 @@ int main() { try { SearchAndMount(); SortByKey(); + OtherTitle(); TrailingWildcards(); + SaveSize(); } 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 e88dc22a86..c117778e49 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN +#include #include #include #else @@ -19,13 +20,16 @@ #include #include #include +#include #include +#include #include extern "C" { extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); +int APS5_VABI sceNetShowNetstatWithMemory(int); 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); @@ -52,6 +56,7 @@ extern const std::uint32_t sce_net_in6addr_any[4]; extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16]; int APS5_VABI sceNetResolverCreate(const char*, int, int); int APS5_VABI sceNetResolverStartNtoa(int, const char*, void*, int, int, int); +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); @@ -60,6 +65,8 @@ int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char*); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI open_nid_postfix(const char*, int, int); @@ -167,6 +174,60 @@ static void CheckStreamSender(int family) { for (const int descriptor : {client, accepted, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static std::string CaptureNetstat(int pool) { + auto* file = std::tmpfile(); + Require(file != nullptr); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + const int output = _fileno(stdout); + const int saved = _dup(output); + Require(saved >= 0 && _dup2(_fileno(file), output) == 0); +#else + const int output = fileno(stdout); + const int saved = dup(output); + Require(saved >= 0 && dup2(fileno(file), output) == output); +#endif + const int result = sceNetShowNetstatWithMemory(pool); + Require(std::fflush(stdout) == 0); +#ifdef _WIN32 + Require(_dup2(saved, output) == 0 && _close(saved) == 0); +#else + Require(dup2(saved, output) == output && close(saved) == 0); +#endif + Require(result == 0 && std::fseek(file, 0, SEEK_SET) == 0); + std::string text; + std::array bytes{}; + for (std::size_t count; (count = std::fread(bytes.data(), 1, bytes.size(), file)) != 0;) text.append(bytes.data(), count); + Require(std::ferror(file) == 0 && std::fclose(file) == 0); + return text; +} + +static void CheckNetstat() { + Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); + const int pool = sceNetPoolCreate("netstat", 4096, 0); + Require(pool > 0); + const auto empty = CaptureNetstat(pool); + Require(empty.find("bytes=4096 sockets=0") != std::string::npos); + const int socket = sceNetSocket("netstat", 2, 1, 0); + Require(socket >= 0); + const int nonblocking = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(socket, address.data(), address.size()) == 0); + Require(sceNetListen(socket, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == address.size()); + const auto before = address; + const auto snapshot = CaptureNetstat(pool); + Require(snapshot.find("bytes=4096 sockets=1") != std::string::npos); + Require(snapshot.find("socket=" + std::to_string(socket) + " family=2 type=1 nonblock=1 bound=1 listening=1") != std::string::npos); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && address == before); + Require(sceNetSocketClose(socket) == 0); + Require(CaptureNetstat(pool).find("sockets=0") != std::string::npos); + Require(sceNetPoolDestroy(pool) == 0); + Require(Failed(sceNetShowNetstatWithMemory(pool), 9)); +} + static void CheckPoolStats() { const int pool = sceNetPoolCreate("stats", 0x4000, 0); Require(pool > 0); @@ -234,6 +295,50 @@ static void CheckEmptyEpoll() { } } +static void CheckDontWait() { + constexpr int DontWait = 0x80; + constexpr int Peek = 0x2; + const int receiver = sceNetSocket("dontwait-receiver", 2, 2, 17); + const int sender = sceNetSocket("dontwait-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0); + const int timeout = 2000000; + Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + 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(); + 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)); + Require(Failed(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek), 35)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sceNetSendto(sender, datagram, sizeof(datagram), DontWait, address.data(), address.size()) == sizeof(datagram)); + Require(sceNetRecvfrom(receiver, buffer, sizeof(buffer), Peek, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait | Peek) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(sceNetRecv(receiver, buffer, sizeof(buffer), DontWait) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(sceNetConnect(sender, address.data(), address.size()) == 0); + 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); +} + static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { const int timeout = 50000; Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); @@ -633,6 +738,73 @@ static void CheckIpv6SocketsAreIpv6Only() { Require(sceNetSocketClose(inet) == 0); } +static void CheckSocketError() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket(nullptr, 2, 1, 6); + Require(listener >= 0 && sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0 && size == address.size()); + const int connected = sceNetSocket(nullptr, 2, 1, 6); + Require(connected >= 0 && sceNetConnect(connected, address.data(), address.size()) == 0); + int error = -1; + size = sizeof(error); + Require(sceNetGetsockopt(connected, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error) && error == 0); + for (const int descriptor : {connected, listener}) Require(sceNetSocketClose(descriptor) == 0); + + const int refused = sceNetSocket(nullptr, 2, 1, 6); + const int nonblocking = 1; + Require(refused >= 0 && sceNetSetsockopt(refused, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + const int started = sceNetConnect(refused, address.data(), address.size()); + Require(started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36); + error = 0; + for (int attempt = 0; attempt < 500 && error == 0; ++attempt) { + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error)); + if (error == 0) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(error == 61); + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && error == 0); + Require(sceNetSocketClose(refused) == 0); +} + +static void CheckDatagramDisconnect() { + const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); + const int other = sceNetSocket("disconnect-other", 2, 2, 17); + const int dissolved = sceNetSocket("disconnect", 2, 2, 17); + Require(peer >= 0 && other >= 0 && dissolved >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + std::array dissolvedAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + Require(sceNetBind_nid_postfix(dissolved, dissolvedAddress.data(), dissolvedAddress.size()) == 0); + std::uint32_t size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0); + size = dissolvedAddress.size(); + Require(sceNetGetsockname(dissolved, dissolvedAddress.data(), &size) == 0); + Require(sceNetConnect(dissolved, peerAddress.data(), peerAddress.size()) == 0); + std::array name{}; + size = name.size(); + Require(sceNetGetpeername(dissolved, name.data(), &size) == 0 && name[2] == peerAddress[2] && name[3] == peerAddress[3]); + const std::array unspecified{16, 0}; + Require(Failed(sceNetConnect(dissolved, unspecified.data(), unspecified.size()), 47)); + size = name.size(); + Require(Failed(sceNetGetpeername(dissolved, name.data(), &size), 57)); + std::array local{}; + size = local.size(); + Require(sceNetGetsockname(dissolved, local.data(), &size) == 0 && local[2] == dissolvedAddress[2] && local[3] == dissolvedAddress[3]); +#ifndef _WIN32 + Require(local[4] == 0 && local[5] == 0 && local[6] == 0 && local[7] == 0); +#endif + const int timeout = 2000000; + Require(sceNetSetsockopt(dissolved, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + const char unrelated[] = "unrelated sender"; + Require(sceNetSendto(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); + char buffer[32]{}; + Require(sceNetRecvfrom(dissolved, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(unrelated)); + Require(std::strcmp(buffer, unrelated) == 0); + for (const int descriptor : {peer, other, dissolved}) Require(sceNetSocketClose(descriptor) == 0); +} + int main() { const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); @@ -648,7 +820,9 @@ int main() { Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); CheckPoolStats(); + CheckNetstat(); CheckEmptyEpoll(); + CheckDontWait(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); @@ -658,6 +832,7 @@ int main() { CheckUnboundSocketName(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); + CheckDatagramDisconnect(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -668,6 +843,7 @@ int main() { Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); + CheckSocketError(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); @@ -837,6 +1013,15 @@ int main() { resolver_error == static_cast(0x804101E1)); Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(ipv4[0] == 127); + std::array resolved6{}; + resolved6.fill(0xA5); + Require(sceNetResolverStartNtoa6(resolver, "::1", resolved6.data(), 5000000, 1, 0) == 0); + Require(std::all_of(resolved6.begin(), resolved6.end() - 1, [](std::uint8_t byte) { return byte == 0; }) && resolved6[15] == 1); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverStartNtoa6(resolver, nullptr, resolved6.data(), 5000000, 1, 0) == static_cast(0x80410116)); + Require(sceNetResolverStartNtoa6(resolver, "127.0.0.1", resolved6.data(), 5000000, 1, 0) == static_cast(0x804101E1)); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); std::array records{}; records.fill(0xA5); @@ -983,6 +1168,26 @@ int main() { Require(sceNetCtlGetIfStat(&second) == 0); Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); } + { + const auto dummyCallback = [](int, void*) {}; + int cid = -1; + Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, nullptr) == static_cast(0x80412107)); + int cids[8] = {}; + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(cids[i] == i); + } + Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlUnregisterCallbackV6(-1) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallbackV6(8) == static_cast(0x80412105)); + Require(sceNetCtlUnregisterCallbackV6(3) == 0); + Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == 0); + Require(cid == 3); + for (int i = 0; i < 8; ++i) { + Require(sceNetCtlUnregisterCallbackV6(i) == 0); + } + } if (ipv6Unavailable) { std::puts("skipped the IPv6 socket checks, the host has no usable IPv6 loopback"); return 77; diff --git a/core/libs/tests/GuestSchedPriority.cpp b/core/libs/tests/GuestSchedPriority.cpp index 0e9d418d11..8da97ce710 100644 --- a/core/libs/tests/GuestSchedPriority.cpp +++ b/core/libs/tests/GuestSchedPriority.cpp @@ -5,7 +5,13 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" { int APS5_VABI sched_get_priority_max_nid_postfix(int policy); int APS5_VABI sched_get_priority_min_nid_postfix(int policy); +int APS5_VABI getpriority_nid_postfix(int which, int who); +int APS5_VABI getpid_nid_postfix(void); +int* APS5_VABI __error_nid_postfix(); } +static constexpr int PrioProcess = 0; +static constexpr int PrioGroup = 1; +static constexpr int PrioUser = 2; static void Require(bool value) { if (!value) std::abort(); } static bool Rejects(int policy) { *__error_nid_postfix() = 0; @@ -13,6 +19,14 @@ static bool Rejects(int policy) { *__error_nid_postfix() = 0; return max && sched_get_priority_min_nid_postfix(policy) == -1 && *__error_nid_postfix() == 22; } +static void RequireNice(int which, int who) { + *__error_nid_postfix() = 13; + Require(getpriority_nid_postfix(which, who) == 0 && *__error_nid_postfix() == 13); +} +static void RequireError(int which, int who, int error) { + *__error_nid_postfix() = 0; + Require(getpriority_nid_postfix(which, who) == -1 && *__error_nid_postfix() == error); +} int main() { Require(Rejects(-1) && Rejects(2) && Rejects(5)); for (int policy : {1, 3}) { @@ -20,4 +34,16 @@ int main() { Require(sched_get_priority_min_nid_postfix(policy) == 256); } Require(Rejects(0) && Rejects(4)); + const int self = getpid_nid_postfix(); + RequireNice(PrioProcess, 0); + RequireNice(PrioProcess, self); + RequireNice(PrioGroup, 0); + RequireNice(PrioGroup, self); + RequireNice(PrioUser, 0); + RequireError(PrioProcess, self + 1, 3); + RequireError(PrioProcess, -1, 3); + RequireError(PrioGroup, self + 1, 3); + RequireError(PrioUser, 1000, 3); + RequireError(3, 0, 22); + RequireError(-1, self, 22); } diff --git a/core/libs/tests/GuestSelectWait.cpp b/core/libs/tests/GuestSelectWait.cpp new file mode 100644 index 0000000000..a21b7b320a --- /dev/null +++ b/core/libs/tests/GuestSelectWait.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int main() { + using Clock = std::chrono::steady_clock; + for (const std::int64_t micros : {0, 50, 500, 1000, 2000, 16667}) { + const GuestTimeval limit{0, micros}; + for (const int count : {0, 1024}) { + const auto start = Clock::now(); + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &limit) == 0, + "select without descriptors did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(micros), + "select returned before its deadline"); + Require(limit.seconds == 0 && limit.microseconds == micros, + "select changed the guest timeout"); + } + } + const GuestTimeval shortLimit{0, 1000}; + const auto shortWaitStart = Clock::now(); + for (int i = 0; i < 20; ++i) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &shortLimit) == 0, + "repeated select without descriptors did not time out"); + } + const auto shortWait = Clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200), + "twenty 1 ms selects did not stay close to their deadlines"); + for (const GuestTimeval limit : {GuestTimeval{-1, 0}, GuestTimeval{0, -1}, GuestTimeval{0, 1000000}}) { + Require(select_nid_postfix(0, nullptr, nullptr, nullptr, &limit) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid timeout"); + } + const GuestTimeval zero{}; + for (const int count : {-1, 1025}) { + Require(select_nid_postfix(count, nullptr, nullptr, nullptr, &zero) == -1 && *__error_nid_postfix() == 22, + "select accepted an invalid descriptor limit"); + } + std::array read{}; + read[0] = 1 | (std::uint64_t{1} << 63); + const GuestTimeval limit{0, 500}; + const auto start = Clock::now(); + Require(select_nid_postfix(1, read.data(), nullptr, nullptr, &limit) == 0, + "select with no ready device did not time out"); + Require(Clock::now() - start >= std::chrono::microseconds(limit.microseconds), + "select with no ready device returned before its deadline"); + Require(read[0] == (std::uint64_t{1} << 63), + "select did not preserve bits outside the descriptor limit"); + std::array write{}; + write[0] = 2; + const GuestTimeval longLimit{1, 0}; + Require(select_nid_postfix(2, nullptr, write.data(), nullptr, &longLimit) == 1 && write[0] == 2, + "select did not report a ready device"); + std::puts("20 select wait scenarios passed"); +} diff --git a/core/libs/tests/GuestServiceDatabase.cpp b/core/libs/tests/GuestServiceDatabase.cpp new file mode 100644 index 0000000000..30caec9095 --- /dev/null +++ b/core/libs/tests/GuestServiceDatabase.cpp @@ -0,0 +1,62 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libScePosixForWebKit/GuestResolver.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" GuestResolver::ServiceEntry* APS5_VABI getservbyname_nid_postfix(const char*, const char*); + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Service database check failed at line %u\n", location.line()); + std::abort(); + } +} + +bool NetworkPort(const GuestResolver::ServiceEntry* entry, std::uint16_t port) { + unsigned char bytes[4]; + std::memcpy(bytes, &entry->port, sizeof(bytes)); + return bytes[0] == port >> 8 && bytes[1] == (port & 0xff) && bytes[2] == 0 && bytes[3] == 0; +} + +int main() { + auto* http = getservbyname_nid_postfix("http", "tcp"); + Require(http && std::strcmp(http->name, "http") == 0 && std::strcmp(http->protocol, "tcp") == 0); + Require(NetworkPort(http, 80)); + Require(http->aliases && std::strcmp(http->aliases[0], "www") == 0); + Require(std::strcmp(http->aliases[1], "www-http") == 0 && http->aliases[2] == nullptr); + + auto* https = getservbyname_nid_postfix("https", nullptr); + Require(https && std::strcmp(https->name, "https") == 0 && std::strcmp(https->protocol, "tcp") == 0); + Require(NetworkPort(https, 443) && https->aliases && https->aliases[0] == nullptr); + + auto* domain = getservbyname_nid_postfix("domain", "udp"); + Require(domain && std::strcmp(domain->name, "domain") == 0 && std::strcmp(domain->protocol, "udp") == 0); + Require(NetworkPort(domain, 53)); + + auto* www = getservbyname_nid_postfix("www", "sctp"); + Require(www && std::strcmp(www->name, "http") == 0 && std::strcmp(www->protocol, "sctp") == 0); + Require(NetworkPort(www, 80)); + + auto* mail = getservbyname_nid_postfix("mail", nullptr); + Require(mail && std::strcmp(mail->name, "smtp") == 0 && NetworkPort(mail, 25)); + + errno = 0; + Require(getservbyname_nid_postfix("no-such-service", nullptr) == nullptr && errno == 0); + Require(getservbyname_nid_postfix("domain", "sctp") == nullptr); + Require(getservbyname_nid_postfix("ntp", "TCP") == nullptr); + Require(getservbyname_nid_postfix("HTTP", "tcp") == nullptr); + + auto* ntp = getservbyname_nid_postfix("ntp", "udp"); + Require(ntp && NetworkPort(ntp, 123)); + std::thread([] { + auto* ssh = getservbyname_nid_postfix("ssh", "tcp"); + Require(ssh && std::strcmp(ssh->name, "ssh") == 0 && NetworkPort(ssh, 22)); + }).join(); + Require(std::strcmp(ntp->name, "ntp") == 0 && std::strcmp(ntp->protocol, "udp") == 0 && NetworkPort(ntp, 123)); + return 0; +} diff --git a/core/libs/tests/GuestSignalList.cpp b/core/libs/tests/GuestSignalList.cpp new file mode 100644 index 0000000000..8035660662 --- /dev/null +++ b/core/libs/tests/GuestSignalList.cpp @@ -0,0 +1,25 @@ +#include +#include + +extern "C" { +extern const char* const sys_siglist_nid_postfix[32]; +extern const int sys_nsig_nid_postfix; +} + +static constexpr int GUEST_NSIG = 32; +static constexpr int GUEST_SIGHUP = 1; +static constexpr int GUEST_SIGINT = 2; +static constexpr int GUEST_SIGSEGV = 11; +static constexpr int GUEST_SIGUSR2 = 31; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + Require(sys_nsig_nid_postfix == GUEST_NSIG); + Require(std::strcmp(sys_siglist_nid_postfix[0], "Signal 0") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGHUP], "Hangup") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGINT], "Interrupt") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGSEGV], "Segmentation fault") == 0); + Require(std::strcmp(sys_siglist_nid_postfix[GUEST_SIGUSR2], "User defined signal 2") == 0); + for (int signal = 0; signal < sys_nsig_nid_postfix; ++signal) Require(sys_siglist_nid_postfix[signal] != nullptr && sys_siglist_nid_postfix[signal][0] != '\0'); +} diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index 798c736518..ddbea3ad02 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -23,11 +24,10 @@ int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSe int APS5_VABI JumpKeepingCalleeSaved(std::uint64_t* buffer); } -#ifdef _WIN32 -#define TEST_ASM_FUNCTION(name) ".globl " name "\n.def " name "; .scl 2; .type 32; .endef\n" name ":\n" -#define TEST_ASM_CALL(name) " call " name "\n" +#define TEST_ASM_FUNCTION(name) APS5_ASM_FUNCTION(name) +#if defined(_WIN32) || defined(__APPLE__) +#define TEST_ASM_CALL(name) " call " APS5_ASM_SYMBOL(name) "\n" #else -#define TEST_ASM_FUNCTION(name) ".globl " name "\n.type " name ", @function\n" name ":\n" #define TEST_ASM_CALL(name) " call " name "@PLT\n" #endif diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 44c0564b44..93a5311f51 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -7,6 +7,9 @@ #include #include using Handler = void (APS5_VABI *)(int); +struct GuestSignalSet { + std::uint32_t bits[4]; +}; extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); @@ -18,10 +21,8 @@ int APS5_VABI pthread_sigmask_nid_postfix(int, const void*, void*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI _is_signal_return_nid_postfix(std::uint64_t); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); } -struct GuestSignalSet { - std::uint32_t bits[4]; -}; struct GuestSigaction { std::uintptr_t handler; int flags; @@ -200,4 +201,24 @@ int main() { Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == static_cast(-1) && current.flags == 0x2); Require(signal_nid_postfix(13, nullptr) == invalid); Require(sigaction_nid_postfix(13, nullptr, ¤t) == 0 && current.handler == 0); + *__error_nid_postfix() = 0; + Require(sigpending_nid_postfix(nullptr) == -1); + Require(*__error_nid_postfix() == 14); + *__error_nid_postfix() = 42; + GuestSignalSet pendingSignals{{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(*__error_nid_postfix() == 42); + Require(signal_nid_postfix(15, nullptr) == Callback); + Require(signal_nid_postfix(15, Callback) == nullptr); + received = 0; + Require(sigprocmask_nid_postfix(1, &termMask, nullptr) == 0); + Require(raise_nid_postfix(15) == 0 && received == 0); + pendingSignals = {{0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == (1u << 14) && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(sigprocmask_nid_postfix(2, &termMask, nullptr) == 0 && received == 15); + Require(sigpending_nid_postfix(&pendingSignals) == 0); + Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + Require(signal_nid_postfix(15, nullptr) == Callback); } diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index 03a9a237b8..52a65f57d5 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -95,10 +95,12 @@ int main() { Require(sigwaitinfo_nid_postfix(nullptr, &info) == -1 && *__error_nid_postfix() == 22); *__error_nid_postfix() = 0; Require(sigwaitinfo_nid_postfix(&empty, &info) == -1 && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(sigwait_nid_postfix(&empty, &taken) == -1 && *__error_nid_postfix() == 22 && taken == -1); - *__error_nid_postfix() = 0; - Require(sigwait_nid_postfix(&interrupt, nullptr) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 71; + Require(sigwait_nid_postfix(nullptr, &taken) == 22 && *__error_nid_postfix() == 71 && taken == -1); + *__error_nid_postfix() = 72; + Require(sigwait_nid_postfix(&empty, &taken) == 22 && *__error_nid_postfix() == 72 && taken == -1); + *__error_nid_postfix() = 73; + Require(sigwait_nid_postfix(&interrupt, nullptr) == 22 && *__error_nid_postfix() == 73); RequireUntouched(info); *__error_nid_postfix() = 0; Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == -1 && *__error_nid_postfix() == 35); diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index ff2b0b5d27..06e23b986e 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -141,6 +141,8 @@ int main(int argc, char** argv) { Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); void* mapping = nullptr; Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + void* twin = nullptr; + Require(sceKernelMapDirectMemory(&twin, page * 2, 3, 0, physical, 0) == 0, "map read/write memory a second time"); auto* buffer = static_cast(mapping) + page - 7; const auto collect = [&] { std::array pages{}; @@ -199,6 +201,9 @@ int main(int argc, char** argv) { void* outputMapping = nullptr; Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, "map sender output memory"); + void* outputTwin = nullptr; + Require(sceKernelMapDirectMemory(&outputTwin, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory a second time"); const auto collectOutputs = [&] { std::array pages{}; std::size_t count = pages.size(); @@ -238,6 +243,7 @@ int main(int argc, char** argv) { if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); } + Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping"); Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); } @@ -300,6 +306,7 @@ int main(int argc, char** argv) { Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(twin, page * 2) == 0, "unmap the second direct memory mapping"); Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); } diff --git a/core/libs/tests/GuestSocketOptions.cpp b/core/libs/tests/GuestSocketOptions.cpp new file mode 100644 index 0000000000..c9b97d1b4a --- /dev/null +++ b/core/libs/tests/GuestSocketOptions.cpp @@ -0,0 +1,227 @@ +#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 listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_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*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI poll_nid_postfix(void*, std::uint32_t, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Stream = 1; +constexpr int Datagram = 2; +constexpr int SolSocket = 0xffff; +constexpr int SoAcceptConnection = 0x2; +constexpr int SoDebug = 0x1; +constexpr int SoKeepAlive = 0x8; +constexpr int SoBroadcast = 0x20; +constexpr int SoUseLoopback = 0x40; +constexpr int SoLinger = 0x80; +constexpr int SoReusePort = 0x200; +constexpr int SoTimestamp = 0x400; +constexpr int SoNoSigpipe = 0x800; +constexpr int SoSendBuffer = 0x1001; +constexpr int SoError = 0x1007; +constexpr int SoType = 0x1008; +constexpr int ErrorInvalid = 22; +constexpr int ErrorDomain = 33; +constexpr int ErrorNoProtocolOption = 42; +constexpr int ErrorConnectionRefused = 61; +constexpr int ErrorBadDescriptor = 9; + +struct Linger { + std::int32_t enabled; + std::int32_t seconds; +}; + +struct PollDescriptor { + std::int32_t descriptor; + std::int16_t events; + std::int16_t revents; +}; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +int GetInt(int descriptor, int option) { + int value = -12345; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(descriptor, SolSocket, option, &value, &size) == 0 && size == sizeof(value), "getsockopt of an integer option failed"); + return value; +} + +void SetInt(int descriptor, int option, int value) { + Require(setsockopt_nid_postfix(descriptor, SolSocket, option, &value, sizeof(value)) == 0, "setsockopt of an integer option failed"); +} + +bool SetFails(int descriptor, int level, int option, const void* value, std::uint32_t length, int error) { + return setsockopt_nid_postfix(descriptor, level, option, value, length) == -1 && *__error_nid_postfix() == error; +} + +std::array Loopback(std::uint16_t portNetworkOrder) { + std::array address{16, 2}; + std::memcpy(address.data() + 2, &portNetworkOrder, 2); + address[4] = 127; + address[7] = 1; + return address; +} + +std::array Listen(int* listener) { + *listener = socket_nid_postfix(2, Stream, 0); + Require(*listener >= 0, "socket failed"); + auto address = Loopback(0); + Require(bind_nid_postfix(*listener, address.data(), address.size()) == 0, "bind failed"); + Require(listen_nid_postfix(*listener, 4) == 0, "listen failed"); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(*listener, address.data(), &size) == 0, "getsockname failed"); + return address; +} + +void WaitUntilSettled(int descriptor) { + PollDescriptor entry{descriptor, 0x1 | 0x4, 0}; + Require(poll_nid_postfix(&entry, 1, 6000) == 1, "the nonblocking connect did not settle within 6 s"); +} + +void ReportsTypeAndListening() { + 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"); + Require(GetInt(stream, SoType) == Stream, "SO_TYPE of a stream socket must be SOCK_STREAM (1)"); + Require(GetInt(datagram, SoType) == Datagram, "SO_TYPE of a datagram socket must be SOCK_DGRAM (2)"); + Require(GetInt(stream, SoAcceptConnection) == 0, "SO_ACCEPTCONN must be 0 before listen"); + auto address = Loopback(0); + Require(bind_nid_postfix(stream, address.data(), address.size()) == 0 && listen_nid_postfix(stream, 4) == 0, "bind or listen failed"); + Require(GetInt(stream, SoAcceptConnection) == SoAcceptConnection, "SO_ACCEPTCONN of a listener is the option bit (2), as FreeBSD returns so_options & SO_ACCEPTCONN"); + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void ReportsAndClearsTheConnectError() { + int listener = -1; + auto address = Listen(&listener); + const int client = socket_nid_postfix(2, Stream, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0, "nonblocking socket setup failed"); + const int result = connect_nid_postfix(client, address.data(), address.size()); + Require(result == 0 || (result == -1 && *__error_nid_postfix() == 36), "nonblocking connect to a listener must succeed or be in progress"); + WaitUntilSettled(client); + Require(GetInt(client, SoError) == 0, "SO_ERROR after a successful connect must be 0"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + for (const int descriptor : {client, accepted, listener}) Require(close_nid_postfix(descriptor) == 0, "close failed"); + + int closed = -1; + auto refused = Listen(&closed); + Require(close_nid_postfix(closed) == 0, "close failed"); + const int failing = socket_nid_postfix(2, Stream, 0); + Require(failing >= 0 && fcntl_nid_postfix(failing, 4, 4) == 0, "nonblocking socket setup failed"); + const int attempt = connect_nid_postfix(failing, refused.data(), refused.size()); + if (attempt == -1 && *__error_nid_postfix() != 36) { + Require(*__error_nid_postfix() == ErrorConnectionRefused, "a refused connect must fail with ECONNREFUSED (61)"); + } else { + WaitUntilSettled(failing); + Require(GetInt(failing, SoError) == ErrorConnectionRefused, "SO_ERROR after a refused connect must be ECONNREFUSED (61)"); + Require(GetInt(failing, SoError) == 0, "reading SO_ERROR must clear it"); + } + Require(close_nid_postfix(failing) == 0, "close failed"); +} + +void BooleanOptionsReportTheirBit() { + 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"); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE is off by default"); + SetInt(stream, SoKeepAlive, 1); + Require(GetInt(stream, SoKeepAlive) == SoKeepAlive, "an enabled boolean option reads back as its own bit (FreeBSD returns so_options & option)"); + SetInt(stream, SoKeepAlive, 0); + Require(GetInt(stream, SoKeepAlive) == 0, "SO_KEEPALIVE must turn off"); + SetInt(datagram, SoBroadcast, 1); + Require(GetInt(datagram, SoBroadcast) == SoBroadcast, "SO_BROADCAST reads back as its bit"); + for (const int option : {SoNoSigpipe, SoReusePort, SoDebug, SoUseLoopback, SoTimestamp}) { + const int one = 1; + Require(SetFails(stream, SolSocket, option, &one, sizeof(one), ErrorNoProtocolOption), "an option no title needs must fail with ENOPROTOOPT (42)"); + int value = 0; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, option, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an option no title needs must fail with ENOPROTOOPT (42)"); + } + Require(close_nid_postfix(stream) == 0 && close_nid_postfix(datagram) == 0, "close failed"); +} + +void LingerFollowsFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + Linger linger{1, 5}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER must be accepted"); + Linger read{}; + std::uint32_t size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && size == sizeof(read), "getsockopt SO_LINGER failed"); + Require(read.enabled == SoLinger && read.seconds == 5, "SO_LINGER reads back as {SO_LINGER bit, seconds}"); + linger = {0, 0}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoLinger, &linger, sizeof(linger)) == 0, "SO_LINGER off must be accepted"); + size = sizeof(read); + Require(getsockopt_nid_postfix(stream, SolSocket, SoLinger, &read, &size) == 0 && read.enabled == 0, "SO_LINGER must read back as off"); + linger = {1, -1}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a negative linger time must fail with EDOM (33)"); + linger = {1, 65536}; + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(linger), ErrorDomain), "a linger time above 65535 must fail with EDOM (33)"); + Require(SetFails(stream, SolSocket, SoLinger, &linger, sizeof(std::int32_t), ErrorInvalid), "a linger value shorter than struct linger must fail with EINVAL (22)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void LengthsFollowFreeBsd() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, SoKeepAlive, &one, 2, ErrorInvalid), "an integer option shorter than an int must fail with EINVAL (22)"); + const std::array wide{1}; + Require(setsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, wide.data(), sizeof(wide)) == 0, "a value longer than an int is read from its first int"); + std::array narrow{0xaa, 0xaa}; + std::uint32_t size = narrow.size(); + Require(getsockopt_nid_postfix(stream, SolSocket, SoKeepAlive, narrow.data(), &size) == 0 && size == narrow.size(), "a short buffer gets a truncated value and its own length (sooptcopyout)"); + Require(narrow[0] == SoKeepAlive && narrow[1] == 0, "the truncated value is the low bytes of the int"); + const int zero = 0; + Require(SetFails(stream, SolSocket, SoSendBuffer, &zero, sizeof(zero), ErrorInvalid), "SO_SNDBUF below 1 must fail with EINVAL (22)"); + const int negative = -5; + Require(SetFails(stream, SolSocket, SoSendBuffer, &negative, sizeof(negative), ErrorInvalid), "a negative SO_SNDBUF must fail with EINVAL (22)"); + SetInt(stream, SoSendBuffer, 65536); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +void UnknownRequestsFail() { + const int stream = socket_nid_postfix(2, Stream, 0); + Require(stream >= 0, "socket failed"); + const int one = 1; + Require(SetFails(stream, SolSocket, 0x7fff, &one, sizeof(one), ErrorNoProtocolOption), "an unknown SOL_SOCKET option must fail with ENOPROTOOPT (42)"); + int value = 0; + std::uint32_t size = sizeof(value); + Require(getsockopt_nid_postfix(stream, SolSocket, 0x7fff, &value, &size) == -1 && *__error_nid_postfix() == ErrorNoProtocolOption, "getsockopt of an unknown option must fail with ENOPROTOOPT (42)"); + Require(setsockopt_nid_postfix(-1, SolSocket, SoKeepAlive, &one, sizeof(one)) == -1 && *__error_nid_postfix() == ErrorBadDescriptor, "setsockopt on a bad descriptor must fail with EBADF (9)"); + Require(close_nid_postfix(stream) == 0, "close failed"); +} + +} + +int main() { + ReportsTypeAndListening(); + ReportsAndClearsTheConnectError(); + BooleanOptionsReportTheirBit(); + LingerFollowsFreeBsd(); + LengthsFollowFreeBsd(); + UnknownRequestsFail(); + return 0; +} diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 3ab148efd5..15dabd1ef6 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -1,3 +1,4 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -38,6 +39,8 @@ int APS5_VABI shutdown_nid_postfix(int, int); int APS5_VABI fcntl_nid_postfix(int, int, ...); 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 __error_nid_postfix(); } @@ -131,6 +134,28 @@ int main() { Require(Exchange(pair[1], pair[0], "wake up")); Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0); + Require(status.st_mode == 0140666 && status.st_size == 0 && status.st_blksize != 0 && status.st_nlink == 0); + Require(send_nid_postfix(pair[0], "hello", 5, 0) == 5 && Readable(pair[1])); + Require(sceKernelFstat(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(shutdown_nid_postfix(pair[0], 1) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == 0 && status.st_mode == 0140444); +#ifndef _WIN32 + Require(shutdown_nid_postfix(pair[1], 0) == 0); + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140666 && status.st_size == 5); + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); +#else + Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == 5); + Require(shutdown_nid_postfix(pair[1], 0) == 0); +#endif + Require(fstat_nid_disambig1_nid_postfix(pair[1], &status) == 0 && status.st_mode == 0140222 && status.st_size == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(pair[0], &status) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFstat(pair[1], &status) == static_cast(0x80020009u)); + Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0); Require(fcntl_nid_postfix(pair[0], 3) == 2); Require(send_nid_postfix(pair[0], "a", 1, 0) == 1); diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 4d89668ab6..1046e7ee55 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -12,7 +12,12 @@ int APS5_VABI sceSslFreeCaCerts(int, void*); int APS5_VABI sceSslGetCaList(int, void*); int APS5_VABI sceSslFreeCaList(int, void*); int APS5_VABI sceSslUnloadCert(int); -int APS5_VABI sceSslSetVerifyCallback(void); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +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); } struct SslMemoryPoolStats { @@ -36,6 +41,8 @@ static bool Throws(TCall call) { return false; } +static int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } + struct SslCaCerts { void* certs; std::size_t num; @@ -51,8 +58,6 @@ int main() { constexpr int notFound = static_cast(0x8095F004); constexpr int invalidArg = static_cast(0x8095177A); int marker = 0; - Require(Throws([] { sceSslSetVerifyCallback(); })); - const int context = sceSslInit_nid_postfix(0x10000); Require(context > 0); SslMemoryPoolStats stats{1, 1, 1, 1}; @@ -90,7 +95,29 @@ int main() { Require(sceSslGetCaList(context, &list) == notFound); Require(list.certs == nullptr && list.num == 0); + constexpr int invalidId = static_cast(0x8095F006); + Require(Throws([&] { sceSslCreateConnection(context, 0, nullptr); })); + Require(sceSslCreateConnection(context + 1000, 0, "example.com") == invalidId); + + const int connection = sceSslCreateConnection(context, 0, "example.com"); + Require(connection > 0 && connection != context); + Require(sceSslSetVerifyCallback(connection, VerifyCallback, &marker) == 0); + Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0); + char buffer[4]{}; + Require(Throws([&] { sceSslConnect(connection); })); + Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); + Require(Throws([&] { sceSslWrite(connection, buffer, sizeof(buffer), 0); })); + + const int unknown = connection + 1000; + Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); + Require(Throws([&] { sceSslDeleteConnection(unknown); })); + Require(sceSslTerm_nid_postfix(context) == 0); + Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); + Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); + Require(sceSslDeleteConnection(connection) == 0); + Require(Throws([&] { sceSslDeleteConnection(connection); })); + Require(sceSslGetCaList(context, nullptr) == invalidArg); Require(sceSslFreeCaList(context, nullptr) == invalidArg); list = {&handle, 3}; diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp new file mode 100644 index 0000000000..aaa93f2962 --- /dev/null +++ b/core/libs/tests/GuestSslLoad.cpp @@ -0,0 +1,176 @@ +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, SceSsl::MemoryPoolStats*); +int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslUnloadCert(int); +int APS5_VABI sceSslGetCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslFreeCaList(int, SceSsl::CaList*); +int APS5_VABI sceSslGetCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +} + +namespace { + +constexpr int NotFound = static_cast(0x8095F004); + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void Throws(TAction action) { + bool caught = false; + try { action(); } catch (const std::exception&) { caught = true; } + Check(caught, "unsupported or malformed load was accepted"); +} + +SceSsl::Data Data(const char* text) { + return {reinterpret_cast(const_cast(text)), std::strlen(text)}; +} + +SceSsl::MemoryPoolStats Stats(int id) { + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(id, &stats) == 0, "stats failed"); + Check(stats.currentInuseSize <= stats.maxInuseSize && stats.maxInuseSize <= stats.poolSize && !stats.reserved, + "pool accounting bounds failed"); + return stats; +} + +void CheckRoot(const SceSsl::CaCerts& snapshot) { + Check(snapshot.num == 1 && snapshot.certs[0].size == RootDer.size(), "CA count or DER size differs"); + Check(std::memcmp(snapshot.certs[0].ptr, RootDer.data(), RootDer.size()) == 0, "CA bytes changed"); +} + +void Lifecycle() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + std::string original = RootPem; + auto data = Data(original.c_str()); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "initial CA load failed"); + original.assign(original.size(), '?'); + Check(Stats(id).currentInuseSize > RootDer.size(), "loaded certificate is not retained/accounted"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "encoded CA query failed"); + CheckRoot(bytes); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::CaList empty{}; + Check(sceSslGetCaList(other, &empty) == NotFound, "CA escaped its context"); + auto foreign = handles; + Throws([&] { sceSslFreeCaList(other, &foreign); }); + Check(sceSslUnloadCert(id) == 0, "unload failed"); + Check(sceSslGetCaList(id, &empty) == NotFound && !empty.certs && !empty.num, "unload retained active CA"); + CheckRoot(bytes); + Check(sceSslFreeCaList(id, &handles) == 0 && !handles.certs && !handles.num, "handle snapshot release failed"); + Check(sceSslFreeCaCerts(id, &bytes) == 0 && !bytes.certs && !bytes.pool && !bytes.num, "encoded snapshot release failed"); + Check(Stats(id).currentInuseSize == 0, "certificate allocation leaked after release"); + Throws([&] { sceSslFreeCaList(id, &foreign); }); + Check(sceSslTerm_nid_postfix(id) == 0 && sceSslTerm_nid_postfix(other) == 0, "term failed"); +} + +void ConnectionLifetime() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + const int connection = sceSslCreateConnection(id, -1, "example.invalid"); + const int other = sceSslInit_nid_postfix(1024 * 1024); + Check(connection > 0 && connection != id && connection != other, "SSL handles overlap"); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load with a connection failed"); + Check(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0, "certificate load invalidated the connection"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA query with a connection failed"); + CheckRoot(bytes); + SceSsl::CaCerts empty{}; + Check(sceSslGetCaCerts(other, &empty) == NotFound, "CA escaped into another context"); + SceSsl::MemoryPoolStats stats{}; + Check(sceSslGetMemoryPoolStats(connection, &stats) == static_cast(0x8095F006), "connection accepted as context"); + const int retained = sceSslCreateConnection(id, -1, "example.invalid"); + Check(retained > 0 && retained != connection && retained != other, "SSL connection handle was reused"); + Check(sceSslDeleteConnection(connection) == 0, "connection deletion failed"); + Check(sceSslUnloadCert(id) == 0, "credential unload failed with a retained connection"); + CheckRoot(bytes); + 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(sceSslGetCaCerts(other, &empty) == NotFound, "unrelated context was invalidated"); + Check(sceSslTerm_nid_postfix(other) == 0, "unrelated context termination failed"); +} + +void AtomicFailures() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + SceSsl::Data der{const_cast(RootDer.data()), RootDer.size()}; + auto* pointer = &der; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "DER CA load failed"); + const auto before = Stats(id).currentInuseSize; + auto bad = Data("not a certificate"); + SceSsl::Data* mixed[]{&der, &bad}; + Throws([&] { sceSslLoadCert(id, 2, mixed, nullptr, nullptr); }); + Check(Stats(id).currentInuseSize == before, "failed chain changed live allocation"); + SceSsl::CaCerts bytes{}; + Check(sceSslGetCaCerts(id, &bytes) == 0, "CA vanished after failure"); + CheckRoot(bytes); + Check(sceSslFreeCaCerts(id, &bytes) == 0, "snapshot free failed"); + Throws([&] { sceSslLoadCert(id, -1, nullptr, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 1, nullptr, nullptr, nullptr); }); + SceSsl::Data* absent = nullptr; + Throws([&] { sceSslLoadCert(id, 1, &absent, nullptr, nullptr); }); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &der, nullptr); }); + Check(Stats(id).currentInuseSize == before, "argument failure changed retained CA"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Throws([&] { sceSslLoadCert(id, 1, &pointer, nullptr, nullptr); }); + const int tiny = sceSslInit_nid_postfix(32); + Throws([&] { sceSslLoadCert(tiny, 1, &pointer, nullptr, nullptr); }); + Check(Stats(tiny).currentInuseSize == 0, "failed small-pool load leaked"); + Check(sceSslTerm_nid_postfix(tiny) == 0, "small-pool term failed"); +} + +void ClientIdentity() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto client = Data(ClientPem); + auto key = Data(ClientKey); + auto wrong = Data(RootKey); + Check(sceSslLoadCert(id, 0, nullptr, &client, &key) == 0, "client identity load failed"); + const auto before = Stats(id).currentInuseSize; + Check(before > 0, "identity not retained"); + Throws([&] { sceSslLoadCert(id, 0, nullptr, &client, &wrong); }); + Check(Stats(id).currentInuseSize == before, "mismatched key changed identity"); + Check(sceSslUnloadCert(id) == 0 && Stats(id).currentInuseSize == 0, "identity release failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); +} + +} + +int main() { + try { + Lifecycle(); + AtomicFailures(); + ClientIdentity(); + ConnectionLifetime(); + std::vector workers; + for (int i = 0; i < 3; ++i) workers.emplace_back([] { for (int n = 0; n < 4; ++n) Lifecycle(); }); + for (auto& worker : workers) worker.join(); + std::puts("SSL load lifecycle passed"); + } catch (const std::exception& error) { + std::fprintf(stderr, "SSL load lifecycle failed: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 0e18fd8fbd..83b97d5ecd 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -429,7 +430,71 @@ static void CheckDescriptorAppend(std::FILE* original) { } } +static void CheckDescriptorAccessModes() { + const auto filename = "anyps5-fdopen-modes-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + for (const int access : {0, 1, 2}) { + for (const char* mode : {"r", "w", "a", "r+", "w+", "a+"}) { + { std::ofstream output(filename); output << "retained"; } +#ifdef _WIN32 + const int descriptor = _open(filename.c_str(), access | _O_BINARY); +#else + const int descriptor = ::open(filename.c_str(), access); +#endif + Require(descriptor >= 0); + const int requested = std::strchr(mode, '+') ? 2 : *mode == 'r' ? 0 : 1; + const bool incompatible = access != 2 && access != requested; +#ifdef _WIN32 + const auto originalHandle = _get_osfhandle(descriptor); + if (incompatible) Require(_lseek(descriptor, 2, SEEK_SET) == 2); +#else + const int originalFlags = ::fcntl(descriptor, F_GETFL); + Require(originalFlags >= 0); + if (incompatible) Require(::lseek(descriptor, 2, SEEK_SET) == 2); +#endif + *__error_nid_postfix() = 0; + auto* stream = fdopen_nid_postfix(descriptor, mode); + if (incompatible) { + if (stream != nullptr || *__error_nid_postfix() != 22) + std::fprintf(stderr, "fdopen access=%d mode=%s returned %p with errno=%d\n", access, mode, + static_cast(stream), *__error_nid_postfix()); + Require(stream == nullptr && *__error_nid_postfix() == 22); +#ifdef _WIN32 + Require(_get_osfhandle(descriptor) == originalHandle && _lseek(descriptor, 0, SEEK_CUR) == 2); +#else + Require(::fcntl(descriptor, F_GETFL) == originalFlags && ::lseek(descriptor, 0, SEEK_CUR) == 2); +#endif + Require(CheckFileBytes(filename, "retained", mode, false)); + const bool probeAppend = access == 1 && *mode == 'a'; +#ifdef _WIN32 + if (probeAppend) Require(_write(descriptor, "X", 1) == 1 && _lseek(descriptor, 0, SEEK_CUR) == 3); + Require(_close(descriptor) == 0); +#else + if (probeAppend) Require(::write(descriptor, "X", 1) == 1 && ::lseek(descriptor, 0, SEEK_CUR) == 3); + Require(::close(descriptor) == 0); +#endif + Require(CheckFileBytes(filename, probeAppend ? "reXained" : "retained", mode, false)); + } else { + Require(stream != nullptr); + if (requested == 0) Require(fgetc_nid_postfix(stream) == 'r'); + else Require(fputc_nid_postfix('X', stream) == 'X'); + Require(fclose_nid_postfix(stream) == 0); + } + } + } +#ifdef _WIN32 + const int closed = _open(filename.c_str(), _O_RDONLY | _O_BINARY); + Require(closed >= 0 && _close(closed) == 0); +#else + const int closed = ::open(filename.c_str(), O_RDONLY); + Require(closed >= 0 && ::close(closed) == 0); +#endif + *__error_nid_postfix() = 0; + Require(fdopen_nid_postfix(closed, "r") == nullptr && *__error_nid_postfix() == 9); + Require(std::filesystem::remove(filename)); +} + int main() { + CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 1ad0f5c6f7..8a30bbf37f 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -22,6 +22,7 @@ int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); int APS5_VABI getpid_nid_postfix(); +std::uint64_t APS5_VABI sceKernelGetTscFrequency(void); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -65,8 +66,8 @@ static void CheckProcessorCountSysctl() { } Require(threw); } -static void CheckPhysicalMemorySysctl() { - const int mib[] = {6, 5}; +static void CheckPhysicalMemorySysctl(int number, const char* name) { + const int mib[] = {6, number}; const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; *__error_nid_postfix() = 13; std::size_t length = 0; @@ -76,15 +77,15 @@ static void CheckPhysicalMemorySysctl() { Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); value = 0; length = 16; - Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(sysctlbyname_nid_postfix(name, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); Require(*__error_nid_postfix() == 13); unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; length = 4; - Require(sysctlbyname_nid_postfix("hw.physmem", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(sysctlbyname_nid_postfix(name, bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); Require(length == 4 && bytes[4] == 0xaa); length = sizeof(value); Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); - Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix(name, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } #if defined(__x86_64__) || defined(__i386__) static void CheckModelSysctl() { @@ -106,6 +107,21 @@ static void CheckModelSysctl() { Require(sysctlbyname_nid_postfix("hw.model", model, &length, model, size) == -1 && *__error_nid_postfix() == 1); } #endif +static void CheckTscFrequencySysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, nullptr, 0) == 0); + Require(value == sceKernelGetTscFrequency() && value != 0 && length == sizeof(value)); + Require(*__error_nid_postfix() == 13); + std::uint32_t truncated = 0; + length = sizeof(truncated); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} static void CheckMachdepCachelineSizeSysctl() { *__error_nid_postfix() = 13; int value = 0x5a5a5a5a; @@ -127,6 +143,10 @@ static void CheckPathnameSysctl() { const int mibTooLong[] = {1, 14, 12, -1, 0}; Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); + const int mibSelf[] = {1, 14, 12, -1}; length = 0; Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); @@ -156,29 +176,40 @@ static void CheckPathnameSysctl() { length = sizeof(buffer); Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } +static void CheckLeafWithTrailingComponent() { + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {1, 37}}; + for (const auto& leaf : leaves) { + const int mib[] = {leaf[0], leaf[1], 0}; + char buffer[8] = {}; + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 20); + Require(sysctl_nid_postfix(mib, 3, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 20); + Require(length == sizeof(buffer)); + } +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); - SceKernelSystemSwVersion version{}; - version.size = 0; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - version.size = sizeof(SceKernelSystemSwVersion) - 1; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - version.size = sizeof(SceKernelSystemSwVersion) + 1; - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == SCE_KERNEL_ERROR_EINVAL && *__error_nid_postfix() == 13); - std::memset(&version, 0xaa, sizeof(version)); - version.size = sizeof(SceKernelSystemSwVersion); - *__error_nid_postfix() = 13; - Require(sceKernelGetSystemSwVersion_nid_postfix(&version) == 0 && *__error_nid_postfix() == 13); - Require(version.size == sizeof(SceKernelSystemSwVersion)); - Require(std::strcmp(version.versionString, "01.000.000") == 0); - for (std::size_t i = sizeof("01.000.000"); i < sizeof(version.versionString); ++i) { - Require(version.versionString[i] == 0); + const std::uint64_t sizes[] = {0, 24, 39, 40, 41, 48, 64}; + for (const std::uint64_t size : sizes) { + struct { + std::uint64_t prefix; + SceKernelSystemSwVersion version; + unsigned char suffix[88]; + } storage; + std::memset(&storage, 0xaa, sizeof(storage)); + storage.version.size = size; + *__error_nid_postfix() = 13; + Require(sceKernelGetSystemSwVersion_nid_postfix(&storage.version) == 0 && *__error_nid_postfix() == 13); + Require(storage.prefix == 0xaaaaaaaaaaaaaaaaull && storage.version.size == size); + Require(std::strcmp(storage.version.versionString, "01.000.000") == 0); + for (std::size_t i = sizeof("01.000.000"); i < sizeof(storage.version.versionString); ++i) { + Require(storage.version.versionString[i] == 0); + } + Require(storage.version.version == 0x01000000); + for (unsigned char byte : storage.suffix) Require(byte == 0xaa); } - Require(version.version == 0x01000000); } static void CheckPseudoRandomSysctl() { *__error_nid_postfix() = 13; @@ -199,6 +230,27 @@ static void CheckPseudoRandomSysctl() { } Require(threw); } +static void CheckOvercommitSysctl() { + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctlbyname_nid_postfix("vm.overcommit", nullptr, &length, nullptr, 0) == 0 && length == sizeof(int)); + int value = -1; + length = sizeof(value); + Require(sysctlbyname_nid_postfix("vm.overcommit", &value, &length, nullptr, 0) == 0 && value == 0 && length == sizeof(int)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("vm.overcommit", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + bool threw = false; + length = sizeof(value); + try { + sysctlbyname_nid_postfix("vm.overcommit", &value, &length, &value, sizeof(value)); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} static void CheckSdkVersionSysctl() { *__error_nid_postfix() = 13; std::size_t length = 0; @@ -251,17 +303,22 @@ static void CheckArandomSysctl() { } int main() { CheckProcessorCountSysctl(); - CheckPhysicalMemorySysctl(); + CheckPhysicalMemorySysctl(5, "hw.physmem"); + CheckPhysicalMemorySysctl(12, "hw.realmem"); #if defined(__x86_64__) || defined(__i386__) CheckModelSysctl(); #endif + CheckTscFrequencySysctl(); CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); + CheckLeafWithTrailingComponent(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); CheckSdkVersionSysctl(); + CheckOvercommitSysctl(); CheckArandomSysctl(); *__error_nid_postfix() = 13; + Require(sysconf_nid_postfix(3) == 128); Require(sysconf_nid_postfix(47) == 0x4000); Require(getpagesize_nid_postfix() == sysconf_nid_postfix(47)); Require(sysconf_nid_postfix(57) > 0); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 56c4146351..2f712816f2 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -20,6 +20,7 @@ extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dsoHandle); extern "C" void APS5_VABI _Exit_nid_postfix(int); +extern "C" void APS5_VABI exit_nid_postfix(int); extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int); using GuestQuickExitCallback = void (APS5_VABI*)(); extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); @@ -71,9 +72,11 @@ void AppendMarker(const std::string& line) { } void MainShutdown() { AppendMarker("shutdown"); } void SecondaryShutdown() { AppendMarker("secondary shutdown"); } -int RunRelaunchingChild(const std::filesystem::path& self) { +void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } +void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } +int RunChild(const std::filesystem::path& self, const char* mode) { #ifdef _WIN32 - std::wstring commandLine = L"\"" + self.wstring() + L"\" --relaunch"; + std::wstring commandLine = L"\"" + self.wstring() + L"\" " + std::wstring(mode, mode + std::strlen(mode)); STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; @@ -88,7 +91,7 @@ int RunRelaunchingChild(const std::filesystem::path& self) { const pid_t child = fork(); Require(child >= 0); if (child == 0) { - execl(self.c_str(), self.c_str(), "--relaunch", static_cast(nullptr)); + execl(self.c_str(), self.c_str(), mode, static_cast(nullptr)); std::_Exit(127); } int status = 0; @@ -96,6 +99,23 @@ int RunRelaunchingChild(const std::filesystem::path& self) { return WIFEXITED(status) ? WEXITSTATUS(status) : 1; #endif } +int DriveNestedExit() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-nested-exit-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + const int code = RunChild(self, "--nested-exit"); + std::string text; + { + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(code == 7); + Require(text == "remaining handler\n"); + std::puts("Guest nested exit completed"); + return 0; +} int DriveRelaunch() { const auto self = OwnExecutable(); const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); @@ -103,7 +123,7 @@ int DriveRelaunch() { const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); SetVariable(RelaunchMarker, marker.string().c_str()); SetVariable(MainExecutable, self.string().c_str()); - Require(RunRelaunchingChild(self) == 0); + Require(RunChild(self, "--relaunch") == 0); std::string text; for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); @@ -140,6 +160,15 @@ int main(int argc, char** argv) { quick_exit_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--nested-exit-driver") == 0) { + return DriveNestedExit(); + } + if (argc > 1 && std::strcmp(argv[1], "--nested-exit") == 0) { + Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { return DriveRelaunch(); } diff --git a/core/libs/tests/GuestSystemServicePathBoundary.cpp b/core/libs/tests/GuestSystemServicePathBoundary.cpp new file mode 100644 index 0000000000..da8e6927f0 --- /dev/null +++ b/core/libs/tests/GuestSystemServicePathBoundary.cpp @@ -0,0 +1,82 @@ +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +[[noreturn]] void UnexpectedLaunch() { + std::_Exit(86); +} + +int Probe(const char* name) { + LibcRegisterShutdown_nid_postfix(UnexpectedLaunch); + const std::string path = std::string("/app0/..\\") + name; + try { + sceSystemServiceLoadExec(path.c_str(), nullptr); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(path) != std::string::npos ? 0 : 2; + } + return 1; +} + +DWORD RunProbe(const std::filesystem::path& executable, const std::string& name) { + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + std::wstring command = L"\"" + executable.wstring() + L"\" --probe "; + command.append(name.begin(), name.end()); + Require(CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + const DWORD waitResult = WaitForSingleObject(process.hProcess, 10000); + if (waitResult != WAIT_OBJECT_0) { + TerminateProcess(process.hProcess, ERROR_TIMEOUT); + WaitForSingleObject(process.hProcess, 5000); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return ERROR_TIMEOUT; + } + DWORD code = 1; + if (!GetExitCodeProcess(process.hProcess, &code)) code = ERROR_GEN_FAILURE; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return code; +} + +int Test() { + std::wstring executable(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length > 0 && length < executable.size()); + executable.resize(length); + const std::filesystem::path own(executable); + const std::string name = "anyps5-path-boundary-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64()); + const auto fixture = own.parent_path().parent_path() / (name + own.extension().string()); + struct Cleanup { + std::filesystem::path path; + ~Cleanup() { + std::error_code error; + std::filesystem::remove(path, error); + } + } cleanup{fixture}; + std::filesystem::copy_file(own, fixture); + Require(RunProbe(own, name) == 0); + return 0; +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--probe") == 0) return Probe(argv[2]); + Require(argc == 1); + return Test(); +} diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 879c664511..edbd4b1f83 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -23,6 +23,7 @@ char* APS5_VABI asctime_nid_postfix(const GuestTm*); 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]; char* APS5_VABI ctime_nid_postfix(const std::int64_t*); int* APS5_VABI __error_nid_postfix(); double APS5_VABI libc_difftime_nid_postfix(std::int64_t, std::int64_t); @@ -400,4 +401,14 @@ int main() { 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 + 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/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index c1b4db8b80..d9da3cf349 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -118,7 +118,7 @@ int main() { char vectorSecond[sizeof(vectorTail)]{}; Iovec vectorInput[]{{vectorFirst, sizeof(vectorFirst)}, {vectorSecond, sizeof(vectorSecond)}}; Require(sceKernelReadv(receiver, vectorInput, 2) == sizeof(vectorHead) + sizeof(vectorTail) - 1); - Require(std::strcmp(vectorFirst, "atomic ") == 0 && std::strcmp(vectorSecond, "datagram") == 0); + Require(std::memcmp(vectorFirst, "atomic ", sizeof(vectorFirst)) == 0 && std::strcmp(vectorSecond, "datagram") == 0); int enabled = 1; Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, sizeof(enabled)) == 0); int option = 0; diff --git a/core/libs/tests/GuestUnwindModuleInfo.cpp b/core/libs/tests/GuestUnwindModuleInfo.cpp index b5af722f49..d0519e368b 100644 --- a/core/libs/tests/GuestUnwindModuleInfo.cpp +++ b/core/libs/tests/GuestUnwindModuleInfo.cpp @@ -2,10 +2,17 @@ #include "tests/GuestUnwindModuleInfoFixture.hpp" #include #include +#include +#include #include +#include #include +#include extern "C" int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleInfoForUnwind* info); +extern "C" void* APS5_VABI dlopen_nid_postfix(const char*, int); +extern "C" void* APS5_VABI dlsym_nid_postfix(void*, const char*); +extern "C" int APS5_VABI dlclose_nid_postfix(void*); static void Require(bool value) { if (!value) std::abort(); } @@ -19,6 +26,54 @@ static bool Throws(std::uint64_t address) { return false; } +struct FixtureCopy { + void* handle; + UnwindFixture* fixture; + std::uint64_t address; +}; + +static std::string FixtureCopyPath() { + HMODULE linked = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(GetUnwindFixture()), &linked)); + wchar_t original[MAX_PATH] = {}; + Require(GetModuleFileNameW(linked, original, MAX_PATH) != 0); + const auto copy = std::filesystem::path(original).replace_filename("unwind_fixture_copy.dll"); + std::filesystem::copy_file(original, copy, std::filesystem::copy_options::overwrite_existing); + return copy.string(); +} + +static FixtureCopy LoadFixtureCopy(const std::string& path) { + void* handle = dlopen_nid_postfix(path.c_str(), 2); + Require(handle != nullptr); + const auto get = reinterpret_cast(dlsym_nid_postfix(handle, "GetUnwindFixture_nid_no_patch")); + Require(get != nullptr); + auto* fixture = get(); + return {handle, fixture, reinterpret_cast(fixture)}; +} + +template +static void LookupOfEditedCopy(const std::string& path, Edit edit, bool throws) { + const auto copy = LoadFixtureCopy(path); + edit(*copy.fixture); + Require(Throws(copy.address) == throws); + Require(dlclose_nid_postfix(copy.handle) == 0); +} + +static void ReloadedModuleIsRead(const std::string& path) { + for (int round = 0; round < 2; ++round) { + const auto copy = LoadFixtureCopy(path); + if (round == 0) { + copy.fixture->frames.terminator = 12; + copy.fixture->frames.spare[0] = 1; + } + ModuleInfoForUnwind info{}; + Require(sceKernelGetModuleInfoForUnwind(copy.address, 0, &info) == 0); + Require(info.eh_frame_size == (round == 0 ? 4 + 12 + 4 + 12 : 4 + 12)); + Require(dlclose_nid_postfix(copy.handle) == 0); + } +} + int main() { auto* fixture = GetUnwindFixture(); Require(fixture != nullptr); @@ -38,21 +93,34 @@ int main() { const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(base + dos->e_lfanew); Require(info.seg0_size == nt->OptionalHeader.SizeOfImage); - - fixture->header.version = 2; - Require(Throws(address)); - fixture->header.version = 1; - for (const std::uint8_t encoding : {0x3b, 0x9b, 0x0f, 0xff}) { - fixture->header.framePointerEncoding = encoding; - Require(Throws(address)); + char mapped[4096] = {}; + Require(GetMappedFileNameA(GetCurrentProcess(), memory.BaseAddress, mapped, sizeof(mapped) - 1) != 0); + Require(std::strncmp(info.name, mapped, sizeof(info.name) - 1) == 0); + for (const auto* inside : {static_cast(&fixture->header), static_cast(&fixture->frames)}) { + ModuleInfoForUnwind other{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(inside), 0, &other) == 0); + Require(std::memcmp(&other, &info, sizeof(info)) == 0); } - fixture->header.framePointerEncoding = 0x1b; - fixture->frames.cieLength = 0xffffffffu; - Require(Throws(address)); - fixture->frames.cieLength = 12; - Require(!Throws(address)); + for (int repeat = 0; repeat < 3; ++repeat) { + ModuleInfoForUnwind again{}; + Require(sceKernelGetModuleInfoForUnwind(address, 0, &again) == 0); + Require(std::memcmp(&again, &info, sizeof(info)) == 0); + } + + const auto path = FixtureCopyPath(); + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.header.version = 2; }, true); + for (const std::uint8_t encoding : {0x3b, 0x9b, 0x0f, 0xff}) { + LookupOfEditedCopy(path, [encoding](UnwindFixture& edited) { edited.header.framePointerEncoding = encoding; }, true); + } + LookupOfEditedCopy(path, [](UnwindFixture& edited) { edited.frames.cieLength = 0xffffffffu; }, true); + LookupOfEditedCopy(path, [](UnwindFixture&) {}, false); + ReloadedModuleIsRead(path); + std::filesystem::remove(path); ModuleInfoForUnwind host{}; Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &host) == 0); Require(host.eh_frame_hdr_addr == 0 && host.eh_frame_addr == 0 && host.eh_frame_size == 0); + ModuleInfoForUnwind hostAgain{}; + Require(sceKernelGetModuleInfoForUnwind(reinterpret_cast(&GetModuleHandleW), 0, &hostAgain) == 0); + Require(std::memcmp(&hostAgain, &host, sizeof(host)) == 0); } diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp index 2e473b4bff..a5abebb36c 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.cpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.cpp @@ -21,3 +21,5 @@ extern "C" UnwindFixture* GetUnwindFixture() { UnwindFixtureMeta = static_cast(reinterpret_cast(&fixture.header) - base); return &fixture; } + +extern "C" UnwindFixture* GetUnwindFixture_nid_no_patch() { return GetUnwindFixture(); } diff --git a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp index d03be890d2..08b2e941d6 100644 --- a/core/libs/tests/GuestUnwindModuleInfoFixture.hpp +++ b/core/libs/tests/GuestUnwindModuleInfoFixture.hpp @@ -16,6 +16,8 @@ struct alignas(4) UnwindFixtureFrames { std::uint32_t cieId = 0; std::uint8_t cie[8] = {1, 0, 1, 0x78, 16, 0, 0, 0}; std::uint32_t terminator = 0; + std::uint32_t spare[3] = {}; + std::uint32_t spareTerminator = 0; }; struct UnwindFixture { diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 41a4b5e0b9..304b77b34d 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -21,6 +21,8 @@ int APS5_VABI setcontext_nid_postfix(const UserUcontext* context); void APS5_VABI makecontext_nid_postfix(UserUcontext* context, ContextEntry entry, int count, ...); [[gnu::returns_twice]] int APS5_VABI setjmp_nid_postfix(std::uint64_t* buffer); [[noreturn]] void APS5_VABI longjmp_nid_postfix(std::uint64_t* buffer, int value); +[[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); +[[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); int* APS5_VABI __error_nid_postfix(); @@ -61,9 +63,9 @@ asm(".text\n" " movabsq $0xcccccccccccccccc, %r11\n" " movabsq $0xdddddddddddddddd, %rdx\n" " movabsq $0xeeeeeeeeeeeeeeee, %rax\n" - " mov %rsp, CapturedStackPointer(%rip)\n" + " mov %rsp, " APS5_ASM_SYMBOL("CapturedStackPointer") "(%rip)\n" " stc\n" - " call getcontext_nid_postfix\n" + " call " APS5_ASM_SYMBOL("getcontext_nid_postfix") "\n" APS5_ASM_FUNCTION("CaptureReturnAddress") " add $8, %rsp\n" " pop %r15\n" @@ -75,50 +77,50 @@ asm(".text\n" " ret\n" APS5_ASM_FUNCTION("RecordRegisters") " pushfq\n" - " mov %rax, RecordedRegisters+0x00(%rip)\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x00(%rip)\n" " pop %rax\n" - " mov %rax, RecordedRegisters+0x88(%rip)\n" - " mov %rbx, RecordedRegisters+0x08(%rip)\n" - " mov %rcx, RecordedRegisters+0x10(%rip)\n" - " mov %rdx, RecordedRegisters+0x18(%rip)\n" - " mov %rsi, RecordedRegisters+0x20(%rip)\n" - " mov %rdi, RecordedRegisters+0x28(%rip)\n" - " mov %rbp, RecordedRegisters+0x30(%rip)\n" - " mov %rsp, RecordedRegisters+0x38(%rip)\n" - " mov %r8, RecordedRegisters+0x40(%rip)\n" - " mov %r9, RecordedRegisters+0x48(%rip)\n" - " mov %r10, RecordedRegisters+0x50(%rip)\n" - " mov %r11, RecordedRegisters+0x58(%rip)\n" - " mov %r12, RecordedRegisters+0x60(%rip)\n" - " mov %r13, RecordedRegisters+0x68(%rip)\n" - " mov %r14, RecordedRegisters+0x70(%rip)\n" - " mov %r15, RecordedRegisters+0x78(%rip)\n" - " stmxcsr RecordedMxcsr(%rip)\n" - " fnstcw RecordedControlWord(%rip)\n" - " movdqu %xmm0, RecordedXmm+0x00(%rip)\n" - " movdqu %xmm1, RecordedXmm+0x10(%rip)\n" - " movdqu %xmm2, RecordedXmm+0x20(%rip)\n" - " movdqu %xmm3, RecordedXmm+0x30(%rip)\n" - " movdqu %xmm4, RecordedXmm+0x40(%rip)\n" - " movdqu %xmm5, RecordedXmm+0x50(%rip)\n" - " movdqu %xmm6, RecordedXmm+0x60(%rip)\n" - " movdqu %xmm7, RecordedXmm+0x70(%rip)\n" - " movdqu %xmm8, RecordedXmm+0x80(%rip)\n" - " movdqu %xmm9, RecordedXmm+0x90(%rip)\n" - " movdqu %xmm10, RecordedXmm+0xa0(%rip)\n" - " movdqu %xmm11, RecordedXmm+0xb0(%rip)\n" - " movdqu %xmm12, RecordedXmm+0xc0(%rip)\n" - " movdqu %xmm13, RecordedXmm+0xd0(%rip)\n" - " movdqu %xmm14, RecordedXmm+0xe0(%rip)\n" - " movdqu %xmm15, RecordedXmm+0xf0(%rip)\n" + " mov %rax, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x88(%rip)\n" + " mov %rbx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x08(%rip)\n" + " mov %rcx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x10(%rip)\n" + " mov %rdx, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x18(%rip)\n" + " mov %rsi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x20(%rip)\n" + " mov %rdi, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x28(%rip)\n" + " mov %rbp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x30(%rip)\n" + " mov %rsp, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x38(%rip)\n" + " mov %r8, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x40(%rip)\n" + " mov %r9, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x48(%rip)\n" + " mov %r10, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x50(%rip)\n" + " mov %r11, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x58(%rip)\n" + " mov %r12, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x60(%rip)\n" + " mov %r13, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x68(%rip)\n" + " mov %r14, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x70(%rip)\n" + " mov %r15, " APS5_ASM_SYMBOL("RecordedRegisters") "+0x78(%rip)\n" + " stmxcsr " APS5_ASM_SYMBOL("RecordedMxcsr") "(%rip)\n" + " fnstcw " APS5_ASM_SYMBOL("RecordedControlWord") "(%rip)\n" + " movdqu %xmm0, " APS5_ASM_SYMBOL("RecordedXmm") "+0x00(%rip)\n" + " movdqu %xmm1, " APS5_ASM_SYMBOL("RecordedXmm") "+0x10(%rip)\n" + " movdqu %xmm2, " APS5_ASM_SYMBOL("RecordedXmm") "+0x20(%rip)\n" + " movdqu %xmm3, " APS5_ASM_SYMBOL("RecordedXmm") "+0x30(%rip)\n" + " movdqu %xmm4, " APS5_ASM_SYMBOL("RecordedXmm") "+0x40(%rip)\n" + " movdqu %xmm5, " APS5_ASM_SYMBOL("RecordedXmm") "+0x50(%rip)\n" + " movdqu %xmm6, " APS5_ASM_SYMBOL("RecordedXmm") "+0x60(%rip)\n" + " movdqu %xmm7, " APS5_ASM_SYMBOL("RecordedXmm") "+0x70(%rip)\n" + " movdqu %xmm8, " APS5_ASM_SYMBOL("RecordedXmm") "+0x80(%rip)\n" + " movdqu %xmm9, " APS5_ASM_SYMBOL("RecordedXmm") "+0x90(%rip)\n" + " movdqu %xmm10, " APS5_ASM_SYMBOL("RecordedXmm") "+0xa0(%rip)\n" + " movdqu %xmm11, " APS5_ASM_SYMBOL("RecordedXmm") "+0xb0(%rip)\n" + " movdqu %xmm12, " APS5_ASM_SYMBOL("RecordedXmm") "+0xc0(%rip)\n" + " movdqu %xmm13, " APS5_ASM_SYMBOL("RecordedXmm") "+0xd0(%rip)\n" + " movdqu %xmm14, " APS5_ASM_SYMBOL("RecordedXmm") "+0xe0(%rip)\n" + " movdqu %xmm15, " APS5_ASM_SYMBOL("RecordedXmm") "+0xf0(%rip)\n" " cld\n" - " lea ReturnContext(%rip), %rdi\n" + " lea " APS5_ASM_SYMBOL("ReturnContext") "(%rip), %rdi\n" " and $-16, %rsp\n" - " call setcontext_nid_postfix\n" + " call " APS5_ASM_SYMBOL("setcontext_nid_postfix") "\n" " ud2\n" APS5_ASM_FUNCTION("EntryThunk") - " mov %rsp, EntryStackPointer(%rip)\n" - " jmp ContextBody\n"); + " mov %rsp, " APS5_ASM_SYMBOL("EntryStackPointer") "(%rip)\n" + " jmp " APS5_ASM_SYMBOL("ContextBody") "\n"); namespace { @@ -150,6 +152,7 @@ int g_entries = 0; Fibre g_dispatcher = {}; Fibre g_job = {}; int g_jobRuns = 0; +bool g_signalJumps = false; void Require(bool value, const char* what) { if (!value) { @@ -348,6 +351,14 @@ namespace { [[gnu::noinline]] void Switch(Fibre* from, Fibre* to) { from->started = 1; + if (g_signalJumps) { + if (sigsetjmp_nid_postfix(from->jump, 1) == 0) { + if (to->started) siglongjmp_nid_postfix(to->jump, 1); + setcontext_nid_postfix(&to->context); + Require(false, "setcontext into the job returned"); + } + return; + } if (setjmp_nid_postfix(from->jump) == 0) { if (to->started) longjmp_nid_postfix(to->jump, 1); setcontext_nid_postfix(&to->context); @@ -360,7 +371,7 @@ namespace { extern "C" void APS5_VABI JobEntry() { for (;;) { ++g_jobRuns; - CheckStackBounds(&g_jobStack, "the stack bounds follow longjmp onto the job stack"); + CheckStackBounds(&g_jobStack, g_signalJumps ? "the stack bounds follow siglongjmp onto the job stack" : "the stack bounds follow longjmp onto the job stack"); ThrowAndCatch(); Switch(&g_job, &g_dispatcher); } @@ -402,7 +413,11 @@ void CheckMakecontext() { #endif } -void CheckJumpBuffers() { +void CheckJumpBuffers(bool signalJumps) { + g_signalJumps = signalJumps; + g_jobRuns = 0; + g_dispatcher = {}; + g_job = {}; #ifdef _WIN32 ULONG_PTR threadLow = 0; ULONG_PTR threadHigh = 0; @@ -420,7 +435,7 @@ void CheckJumpBuffers() { ULONG_PTR low = 0; ULONG_PTR high = 0; GetCurrentThreadStackLimits(&low, &high); - Require(low == threadLow && high == threadHigh, "the stack bounds follow longjmp back to the thread stack"); + Require(low == threadLow && high == threadHigh, signalJumps ? "the stack bounds follow siglongjmp back to the thread stack" : "the stack bounds follow longjmp back to the thread stack"); #endif } } @@ -466,7 +481,8 @@ int main(int argc, char** argv) { CheckSignalMask(); std::thread([] { CheckMakecontext(); - CheckJumpBuffers(); + CheckJumpBuffers(false); + CheckJumpBuffers(true); GrowStack(32); }).join(); return 0; diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index da708754d1..4bed080a11 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -21,6 +21,7 @@ static constexpr int MAIN_BUS = 0; static constexpr int INVALID_VALUE = static_cast(0x80290001); static constexpr int INVALID_ADDRESS = static_cast(0x80290002); static constexpr int INVALID_HANDLE = static_cast(0x8029000B); +static constexpr int RESOURCE_BUSY = static_cast(0x80290009); static constexpr int UNSUPPORTED_OUTPUT_MODE = static_cast(0x80290016); static constexpr int UNKNOWN_OUTPUT_MODE = static_cast(0x8029001E); @@ -50,6 +51,7 @@ int main() { return 77; } Require(handle > 0); + Require(sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr) == RESOURCE_BUSY); Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); diff --git a/core/libs/tests/GuestVideoOutResolutionStatus.cpp b/core/libs/tests/GuestVideoOutResolutionStatus.cpp index 5294d7c29f..e401ad3023 100644 --- a/core/libs/tests/GuestVideoOutResolutionStatus.cpp +++ b/core/libs/tests/GuestVideoOutResolutionStatus.cpp @@ -1,6 +1,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/Shutdown.hpp" #include "tests/VideoOutTestEnvironment.hpp" +#include +#include #include #include #include @@ -10,6 +12,7 @@ extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param); int APS5_VABI sceVideoOutClose(int handle); int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status); +int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status); int APS5_VABI sceVideoOutGetResolutionStatus(int handle, VideoOutResolutionStatus* status); } @@ -52,6 +55,18 @@ int main() { Require(sceVideoOutGetOutputStatus(handle, &output) == 0); Require(status.refreshRate == output.refreshRate); + std::array vrr; + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == 0); + Require(std::memcmp(vrr.data(), &output, sizeof(output)) == 0); + for (std::size_t offset = sizeof(output); offset < 80; ++offset) Require(vrr[offset] == std::byte{0}); + for (std::size_t offset = 80; offset < vrr.size(); ++offset) Require(vrr[offset] == std::byte{0xff}); + vrr.fill(std::byte{0xff}); + Require(sceVideoOutGetOutputStatusWithVrr(handle, nullptr) == static_cast(0x80290002u)); + Require(sceVideoOutGetOutputStatusWithVrr(NEVER_OPENED_HANDLE, vrr.data()) == static_cast(0x8029000Bu)); + Require(sceVideoOutGetOutputStatusWithVrr(-1, vrr.data()) == static_cast(0x8029000Bu)); + for (const auto byte : vrr) Require(byte == std::byte{0xff}); + Require(Rejects(handle, nullptr)); Require(Rejects(0, &status)); Require(Rejects(-1, &status)); @@ -59,5 +74,6 @@ int main() { Require(sceVideoOutClose(handle) == 0); Require(Rejects(handle, &status)); + Require(sceVideoOutGetOutputStatusWithVrr(handle, vrr.data()) == static_cast(0x8029000Bu)); LibcRunShutdown_nid_postfix(); } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index fa5b81cac0..8cd823acbe 100644 --- a/core/libs/tests/GuestWideConversion.cpp +++ b/core/libs/tests/GuestWideConversion.cpp @@ -11,6 +11,7 @@ extern "C" { std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t*, char16_t**, int); int APS5_VABI _Iswctype_nid_postfix(std::uint32_t, std::uint64_t); +int APS5_VABI swscanf_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI swscanf_s_nid_postfix(const char16_t*, const char16_t*, ...); int APS5_VABI iswalnum_nid_postfix(std::uint32_t); int APS5_VABI iswalpha_nid_postfix(std::uint32_t); @@ -155,9 +156,66 @@ static void CheckScan() { Require(Throws([&] { swscanf_s_nid_postfix(u"Ā", u"%s", narrow, std::size_t{4}); }), "unconvertible narrow %s throws"); } +static void CheckUnsizedScan() { + int first = 0; + int second = 0; + Require(swscanf_nid_postfix(u" 42 -7", u"%d%d", &first, &second) == 2 && first == 42 && second == -7, "swscanf %d"); + Require(swscanf_nid_postfix(u"x=0x1f", u"x=%i", &first) == 1 && first == 31, "swscanf %i with a hex prefix"); + Require(swscanf_nid_postfix(u"1 2", u"%*d%d", &first) == 1 && first == 2, "swscanf suppressed %d"); + Require(swscanf_nid_postfix(u"12345", u"%3d%d", &first, &second) == 2 && first == 123 && second == 45, "swscanf %d width"); + std::int64_t wide = 0; + Require(swscanf_nid_postfix(u"5000000000", u"%ld", &wide) == 1 && wide == 5000000000LL, "swscanf %ld is 64-bit"); + short half = 0; + 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"); + + char16_t text[16] = {u'?'}; + Require(swscanf_nid_postfix(u"hello world", u"%ls", text) == 1 && std::u16string(text) == u"hello", "swscanf %ls"); + Require(swscanf_nid_postfix(u"7 a-long-word", u"%d%ls", &first, text) == 2 && first == 7 && std::u16string(text) == u"a-long-word", "swscanf %ls takes no size"); + Require(swscanf_nid_postfix(u"hello world", u"%3ls", text) == 1 && std::u16string(text) == u"hel", "swscanf %ls width"); + char narrow[8] = {}; + Require(swscanf_nid_postfix(u"abc def", u"%s%d", narrow, &first) == 1 && std::strcmp(narrow, "abc") == 0, "swscanf %s"); + char16_t pair[3] = {u'?', u'?', u'?'}; + Require(swscanf_nid_postfix(u"xyz", u"%2lc", pair) == 1 && pair[0] == u'x' && pair[1] == u'y' && pair[2] == u'?', "swscanf %2lc"); + Require(swscanf_nid_postfix(u"x", u"%2lc", pair) == 0, "swscanf %2lc short input"); + char letter = 0; + Require(swscanf_nid_postfix(u" q7", u"%c%d", &letter, &first) == 1 && letter == ' ', "swscanf %c does not skip white space"); + Require(swscanf_nid_postfix(u"cab12", u"%l[abc]%d", text, &first) == 2 && std::u16string(text) == u"cab" && first == 12, "swscanf %l["); + Require(swscanf_nid_postfix(u"été!", u"%l[^!]", text) == 1 && std::u16string(text) == u"été", "swscanf %l[^"); + Require(swscanf_nid_postfix(u"key=val", u"%[^=]=%s", narrow, narrow + 4) == 2 && std::strcmp(narrow, "key") == 0 && std::strcmp(narrow + 4, "val") == 0, "swscanf %[^"); + std::int64_t count = -1; + Require(swscanf_nid_postfix(u" 12 ab", u"%d %ln", &first, &count) == 1 && count == 4, "swscanf %ln"); + int shortCount = -1; + Require(swscanf_nid_postfix(u"abc", u"%*ls%n", &shortCount) == 0 && shortCount == 3, "swscanf %n after a suppressed conversion"); + Require(swscanf_nid_postfix(u"50% off", u"%d%% off", &first) == 1 && first == 50, "swscanf %%"); + Require(swscanf_nid_postfix(u"50 off", u"%d%%", &first) == 1, "swscanf %% mismatch is a matching failure"); + + double real = 0; + Require(swscanf_nid_postfix(u"1.5e3", u"%lf", &real) == 1 && real == 1500.0, "swscanf %lf"); + Require(swscanf_nid_postfix(u"1e+x", u"%lf", &real) == 0, "swscanf %lf incomplete exponent is a matching failure"); + long double extended = 0; + Require(swscanf_nid_postfix(u"2.25", u"%Lf", &extended) == 1 && extended == 2.25L, "swscanf %Lf"); + float single = 0; + Require(swscanf_nid_postfix(u"-inf", u"%f", &single) == 1 && single == -std::numeric_limits::infinity(), "swscanf %f infinity"); + + Require(swscanf_nid_postfix(u"a", u"b%d", &first) == 0, "swscanf literal mismatch is a matching failure"); + Require(swscanf_nid_postfix(u"3 x", u"%d%d", &first, &second) == 1 && first == 3, "swscanf partial match"); + Require(swscanf_nid_postfix(u"", u"%d", &first) == -1, "swscanf empty input is an input failure"); + Require(swscanf_nid_postfix(u"", u"%ls", text) == -1, "swscanf empty input before %ls"); + Require(swscanf_nid_postfix(u" ", u"%ls", text) == -1, "swscanf white space only is an input failure"); + Require(swscanf_nid_postfix(u"5", u"%d%d", &first, &second) == 1, "swscanf input failure after a conversion"); + Require(swscanf_nid_postfix(nullptr, u"%d", &first) == -1, "swscanf null buffer"); + 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"Ā", u"%s", narrow); }), "swscanf unconvertible narrow %s throws"); +} + int main() { CheckStoul(); CheckClassification(); CheckScan(); + CheckUnsizedScan(); return failures == 0 ? 0 : 1; } diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index adba600ad6..f372fd47e1 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -10,6 +10,9 @@ extern "C" int APS5_VABI vswprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI snwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vswprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); +extern "C" int APS5_VABI swprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI vsnwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI swprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); extern "C" std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); extern "C" std::size_t APS5_VABI wcsftime_nid_postfix(char16_t*, std::size_t, const char16_t*, const GuestTm*); @@ -148,6 +151,38 @@ static void CheckGroupingFlag() { "snwprintf_s ' flag"); } +static void CheckSecureWideBoundary() { + constexpr std::size_t wideRsizeMax = (SIZE_MAX >> 1) / sizeof(char16_t); + char16_t buffer[4]; + for (auto& unit : buffer) unit = u'x'; + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n") < 0 && buffer[0] == 0, "swprintf_s accepted-boundary failure clears"); + Require(buffer[1] == u'y', "swprintf_s does not clear beyond the first unit"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(swprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n") < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "swprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vswprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vswprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vswprintf_s oversized size leaves the buffer untouched"); + + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax, u"%n", nullptr) < 0 && buffer[0] == 0, "vsnwprintf_s accepted-boundary failure clears"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(vsnwprintf_s_nid_postfix(buffer, wideRsizeMax + 1, u"%n", nullptr) < 0 && buffer[0] == u'x' && buffer[1] == u'y', + "vsnwprintf_s oversized size leaves the buffer untouched"); +} + static void CheckSwprintf() { char16_t buffer[8]; for (auto& unit : buffer) unit = u'x'; @@ -244,6 +279,7 @@ int main() { CheckPointer(); CheckCount(); CheckSwprintf(); + CheckSecureWideBoundary(); CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); diff --git a/core/libs/tests/HostCpu.cpp b/core/libs/tests/HostCpu.cpp new file mode 100644 index 0000000000..7c5d3aea88 --- /dev/null +++ b/core/libs/tests/HostCpu.cpp @@ -0,0 +1,139 @@ +#include "prx/libkernel/System/include/HostCpu.hpp" +#include +#include +#include +#include +#include + +static void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +static constexpr std::uint32_t Bit(unsigned bit) { + return std::uint32_t{1} << bit; +} + +static HostCpuRegisters Zen2() { + HostCpuRegisters registers; + registers.Leaf1Ecx = Bit(0) | Bit(1) | Bit(9) | Bit(12) | Bit(13) | Bit(19) | Bit(20) | Bit(22) | Bit(23) | Bit(25) | Bit(26) | Bit(27) | Bit(28) | Bit(29) | Bit(30); + registers.Leaf1Edx = Bit(25) | Bit(26); + registers.Leaf7Ebx = Bit(3) | Bit(5) | Bit(8) | Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29); + registers.Leaf7Ecx = Bit(22); + registers.LeafDSub1Eax = Bit(0) | Bit(1); + registers.Leaf80000001Ecx = Bit(0) | Bit(5) | Bit(6) | Bit(8); + registers.Leaf80000008Ebx = Bit(0); + registers.Xcr0 = 0x7; + return registers; +} + +static std::string Text(const std::vector& names) { + std::string text; + for (const auto& name : names) text += (text.empty() ? "" : ",") + name; + return text; +} + +static void CheckMissing(const HostCpuRegisters& registers, const std::vector& expected, const std::string& label) { + const auto missing = MissingHostCpuFeatures(registers); + Require(missing == expected, label + ": expected [" + Text(expected) + "], got [" + Text(missing) + "]"); +} + +static void CheckZen2() { + CheckMissing(Zen2(), {}, "Zen 2"); + for (const auto& feature : HostCpuFeatures(Zen2())) + Require(feature.Present, std::string("Zen 2 lacks ") + feature.Name); +} + +static void CheckIntelWithoutAmdOnlyExtensions() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~(Bit(18) | Bit(19) | Bit(23) | Bit(24) | Bit(29)); + registers.Leaf7Ecx = 0; + registers.LeafDSub1Eax = Bit(0); + registers.Leaf80000001Ecx &= ~(Bit(6) | Bit(8)); + registers.Leaf80000008Ebx = 0; + CheckMissing(registers, {}, "AVX2 host without SSE4A, CLZERO, SHA, ADX, RDSEED, CLFLUSHOPT, CLWB, RDPID"); +} + +static void CheckSingleMissingExtension() { + struct Case { + const char* Name; + std::uint32_t HostCpuRegisters::* Register; + unsigned Bit; + }; + const Case cases[] = { + {"BMI2", &HostCpuRegisters::Leaf7Ebx, 8}, + {"BMI1", &HostCpuRegisters::Leaf7Ebx, 3}, + {"AVX2", &HostCpuRegisters::Leaf7Ebx, 5}, + {"MOVBE", &HostCpuRegisters::Leaf1Ecx, 22}, + {"FMA", &HostCpuRegisters::Leaf1Ecx, 12}, + {"F16C", &HostCpuRegisters::Leaf1Ecx, 29}, + {"POPCNT", &HostCpuRegisters::Leaf1Ecx, 23}, + {"SSE4.2", &HostCpuRegisters::Leaf1Ecx, 20}, + {"CMPXCHG16B", &HostCpuRegisters::Leaf1Ecx, 13}, + {"LZCNT", &HostCpuRegisters::Leaf80000001Ecx, 5}, + {"LAHF/SAHF", &HostCpuRegisters::Leaf80000001Ecx, 0}, + }; + for (const auto& entry : cases) { + auto registers = Zen2(); + registers.*entry.Register &= ~Bit(entry.Bit); + CheckMissing(registers, {entry.Name}, std::string("without ") + entry.Name); + } +} + +static void CheckAvxWithoutOsState() { + auto registers = Zen2(); + registers.Xcr0 = 0x3; + CheckMissing(registers, {"OS AVX state (XCR0)"}, "XCR0 without YMM state"); + registers = Zen2(); + registers.Leaf1Ecx &= ~Bit(27); + CheckMissing(registers, {"OS AVX state (XCR0)"}, "OSXSAVE clear"); +} + +static void CheckPreHaswell() { + auto registers = Zen2(); + registers.Leaf1Ecx &= ~(Bit(12) | Bit(22)); + registers.Leaf7Ebx = 0; + registers.Leaf80000001Ecx &= ~Bit(5); + CheckMissing(registers, {"LZCNT", "BMI1", "BMI2", "MOVBE", "AVX2", "FMA"}, "AVX host without AVX2"); +} + +static void CheckError() { + auto registers = Zen2(); + registers.Leaf7Ebx &= ~Bit(8); + CheckHostCpu(Zen2(), "AMD Ryzen 7 3700X 8-Core Processor"); + try { + CheckHostCpu(registers, "Example CPU"); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(Example CPU) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, "AMD Example "); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(AMD Example) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } + try { + CheckHostCpu(registers, ""); + throw std::logic_error("missing BMI2 did not throw"); + } catch (const std::runtime_error& error) { + const std::string message = error.what(); + Require(message.find("(unknown model) lacks BMI2.") != std::string::npos, "unexpected message: " + message); + } +} + +int main() { + try { + CheckZen2(); + CheckIntelWithoutAmdOnlyExtensions(); + CheckSingleMissingExtension(); + CheckAvxWithoutOsState(); + CheckPreHaswell(); + CheckError(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 95d3352628..6fa633e3a7 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -28,6 +28,30 @@ static void CheckPck(std::uint32_t encoding, RdnaOpcode opcode, std::uint32_t dm Require(instruction.dataDwordCount == dataComponents); Require(IsImageOpcode(instruction.op)); } +static void CheckSamplerVccTuple() { + const std::array code{0xf0800f08u, 26u << 21u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.source1.reg == 0u); + Require(instruction.source2.kind == RdnaOperandKind::ScalarRegister && instruction.source2.reg == 104u); + 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().sampler == 26u); +} +static void CheckSamplerVccTupleOverflow() { + const std::array code{0xf0800f08u, 27u << 21u}; + bool rejected = false; + try { + DecodeRdnaMimg(0u, code, 0u); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} static void CheckPckRejectsZeroMask(std::uint32_t encoding) { const std::array code{(0x3cu << 26u) | (encoding << 18u) | (1u << 3u), 0u}; bool rejected = false; @@ -49,6 +73,8 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string Require(false); } int main() { + CheckSamplerVccTuple(); + CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); Check(0x35u, RdnaOpcode::ImageSampleBO, 0xfu); diff --git a/core/libs/tests/LinearDisplayBuffer.cpp b/core/libs/tests/LinearDisplayBuffer.cpp index d34623bd0e..ff5e5da9b8 100644 --- a/core/libs/tests/LinearDisplayBuffer.cpp +++ b/core/libs/tests/LinearDisplayBuffer.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" #include "prx/libSceAgcDriver/Graphics/include/HalfFloatScanout.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include @@ -86,6 +87,89 @@ static void HalfFloatDisplay() { } } +static double ReferencePqNits(std::uint32_t code) { + const double m1 = 2610.0 / 16384.0, m2 = 2523.0 / 4096.0 * 128.0, c1 = 3424.0 / 4096.0, c2 = 2413.0 / 4096.0 * 32.0, c3 = 2392.0 / 4096.0 * 32.0; + const double e = std::pow(code / 1023.0, 1.0 / m2); + return 10000.0 * std::pow(std::max(e - c1, 0.0) / (c2 - c3 * e), 1.0 / m1); +} + +static std::array ReferenceSdr(std::uint32_t red, std::uint32_t green, std::uint32_t blue, double whiteNits) { + const double referenceWhite = std::min(203.0, whiteNits); + const double r = ReferencePqNits(red) / referenceWhite, g = ReferencePqNits(green) / referenceWhite, b = ReferencePqNits(blue) / referenceWhite; + std::array rgb{std::max(1.6604910 * r - 0.5876411 * g - 0.0728499 * b, 0.0), std::max(-0.1245505 * r + 1.1328999 * g - 0.0083494 * b, 0.0), std::max(-0.0181508 * r - 0.1005789 * g + 1.1187297 * b, 0.0)}; + const double luminance = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; + const double knee = 0.75; + if (luminance > knee) { + const double t = (luminance - knee) / (1.0 - knee), w = (whiteNits / referenceWhite - knee) / (1.0 - knee); + const double mapped = knee + (1.0 - knee) * t * (1.0 + t / (w * w)) / (1.0 + t); + for (auto& channel : rgb) channel *= mapped / luminance; + } + for (auto& channel : rgb) { + const double v = std::clamp(channel, 0.0, 1.0); + channel = 255.0 * (v <= 0.0031308 ? 12.92 * v : 1.055 * std::pow(v, 1.0 / 2.4) - 0.055); + } + return rgb; +} + +static std::uint32_t PqWord(std::uint32_t red, std::uint32_t green, std::uint32_t blue, bool redLow) { + return (redLow ? red : blue) | (green << 10u) | ((redLow ? blue : red) << 20u) | (3u << 30u); +} + +static void PqDisplay() { + constexpr std::uint64_t rgbPq = 0x8100070422000000ull; + constexpr std::uint64_t bgrPq = 0x8100070400000000ull; + for (const auto pixelFormat : {rgbPq, bgrPq}) { + const bool redLow = pixelFormat == rgbPq; + Require(AgcDriver::DisplayPq(pixelFormat) && AgcDriver::DisplayTenBit(pixelFormat) && !AgcDriver::DisplayHalfFloat(pixelFormat) && AgcDriver::DisplayRedLow(pixelFormat) == redLow); + Require(AgcDriver::DisplayTexelBytes(pixelFormat) == 4 && AgcDriver::DisplayTexelFormat(pixelFormat) == (redLow ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 : VK_FORMAT_A2R10G10B10_UNORM_PACK32)); + for (const auto storage : {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32}) Require(AgcDriver::ResidentPresentPath(storage, pixelFormat, true) == AgcDriver::ResidentPresent::Convert); + Require(AgcDriver::ResidentPresentPath(VK_FORMAT_B8G8R8A8_UNORM, pixelFormat, true) == AgcDriver::ResidentPresent::None && AgcDriver::ResidentPresentPath(VK_FORMAT_R16G16B16A16_SFLOAT, pixelFormat, true) == AgcDriver::ResidentPresent::None); + Require(AgcDriver::DisplayBufferSize({65536, pixelFormat, 3840, 2160}) == 30u * 17u * 65536u); + struct Anchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array anchors{{{0, 0, 0, 0, 0, 0}, {1023, 1023, 1023, 255, 255, 255}, {520, 520, 520, 255, 255, 255}, {450, 450, 450, 187, 187, 187}, + {600, 500, 300, 255, 213, 0}, {480, 470, 350, 225, 204, 95}, {350, 420, 500, 16, 167, 241}, {1023, 0, 0, 255, 0, 0}}}; + std::array input{}; + for (std::size_t i = 0; i < anchors.size(); ++i) { + const auto word = PqWord(anchors[i].red, anchors[i].green, anchors[i].blue, redLow); + std::memcpy(input.data() + i * 4, &word, 4); + } + const AgcDriver::DisplayBuffer buffer{65536, pixelFormat, 4, 2, 1, 0}; + Require(AgcDriver::DisplayBufferSize(buffer) == input.size()); + const auto pixels = AgcDriver::DecodeDisplayBuffer(buffer, input); + for (std::size_t i = 0; i < anchors.size(); ++i) { + Require(std::to_integer(pixels[i * 4]) == anchors[i].b && std::to_integer(pixels[i * 4 + 1]) == anchors[i].g && std::to_integer(pixels[i * 4 + 2]) == anchors[i].r && std::to_integer(pixels[i * 4 + 3]) == 255); + } + } + struct WhiteAnchor { + std::uint32_t red, green, blue; + unsigned r, g, b; + }; + const std::array shoulder{{{770, 770, 770, 255, 255, 255}, {700, 700, 700, 252, 252, 252}, {650, 650, 650, 249, 249, 249}, {560, 560, 560, 221, 221, 221}, {450, 450, 450, 136, 136, 136}, {600, 500, 300, 255, 155, 0}}}; + for (const auto& anchor : shoulder) { + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(anchor.red, anchor.green, anchor.blue, true), true, 1000.0f); + Require(pixel[2] == anchor.r && pixel[1] == anchor.g && pixel[0] == anchor.b && pixel[3] == 255); + } + for (const auto pixelFormat : {0x8100070000000000ull, 0x8100000400000000ull, 0x8000070400000000ull, 0x8100070400000001ull, 0x8100070600000000ull, 0xc001070700000000ull}) { + Require(!AgcDriver::DisplayPq(pixelFormat)); + Reject([&] { AgcDriver::DisplayBufferSize({65536, pixelFormat, 64, 64}); }); + } + for (const float white : {100.0f, 150.0f, 1000.0f}) { + for (std::uint32_t red = 0; red < 1024; red += 31) { + for (std::uint32_t green = 0; green < 1024; green += 31) { + for (std::uint32_t blue = 0; blue < 1024; blue += 31) { + const auto expected = ReferenceSdr(red, green, blue, white); + const auto pixel = AgcDriver::PqToSdrPixel(PqWord(red, green, blue, true), true, white); + Require(std::fabs(pixel[2] - expected[0]) <= 1.0 && std::fabs(pixel[1] - expected[1]) <= 1.0 && std::fabs(pixel[0] - expected[2]) <= 1.0 && pixel[3] == 255); + Require(AgcDriver::PqToSdrPixel(PqWord(red, green, blue, false), false, white) == pixel); + } + } + } + } +} + int main() { AgcDriver::DisplayBuffer buffer{65536, 0x8000000000000000ull, 2, 2, 1, 3}; const std::array input{1,2,3,4, 5,6,7,8, 99,99,99,99, @@ -110,4 +194,5 @@ int main() { Require(AgcDriver::DisplayBufferSize(buffer) == 65536); HalfFloatCodes(); HalfFloatDisplay(); + PqDisplay(); } diff --git a/core/libs/tests/MemoryPoolTest.cpp b/core/libs/tests/MemoryPoolTest.cpp new file mode 100644 index 0000000000..4134c904a7 --- /dev/null +++ b/core/libs/tests/MemoryPoolTest.cpp @@ -0,0 +1,341 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +bool DirectMemoryCheckedFree(int64_t start, size_t len); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); +size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPool = static_cast(DIRECT_MEMORY_SIZE); + +enum class Fault { None, BackingBadAlloc, BackingRuntimeError, ForgetRuntimeError }; +Fault g_fault = Fault::None; +long g_newBudget = -1; + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf(" FAILED line %d: %s\n", __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +struct NewBudget { + explicit NewBudget(long n) { g_newBudget = n; } + ~NewBudget() { g_newBudget = -1; } +}; + +bool BlockAt(uint64_t offset, int64_t& s, int64_t& e, int& t) { + return DirectMemoryFind(static_cast(offset), false, &s, &e, &t); +} + +bool BlockIs(uint64_t offset, uint64_t start, uint64_t end, int type) { + int64_t s = 0, e = 0; + int t = 0; + return BlockAt(offset, s, e, t) && s == static_cast(start) && e == static_cast(end) && t == type; +} + +bool PoolEmpty() { + int64_t s, e; + int t; + return !DirectMemoryFind(0, true, &s, &e, &t) && DirectMemoryFreeRun(0, kPool) == kPool; +} + +void alloc_unplaceable_requests() { + int64_t out = -1; + CHECK(DirectMemoryAlloc(0, kPool, 0, 0, 1, &out) == SCE_KERNEL_ERROR_EINVAL); + CHECK(DirectMemoryAlloc(-1, kPool, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, SIZE_MAX, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(0, kPool, kPool + kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(2 * kPage, kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(DirectMemoryAlloc(kPool, kPool + kPage, kPage, 0, 1, &out) == SCE_KERNEL_ERROR_EAGAIN); + CHECK(out == -1); + CHECK(PoolEmpty()); +} + +void alloc_null_out() { + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + CHECK(PoolEmpty()); +} + +void alloc_rounding_alignment_and_clamping() { + int64_t a = -1, b = -1; + CHECK(DirectMemoryAlloc(0, kPool, 1, 1, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, -1, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + CHECK(DirectMemoryAlloc(0, INT64_MAX, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + + CHECK(DirectMemoryAlloc(1, kPool, kPage, 0, 1, &a) == 0 && a == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &b) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_non_power_of_two_alignment() { + int64_t a = -1, b = -1, c = -1; + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &a) == 0 && a == static_cast(3 * kPage)); + CHECK(DirectMemoryAlloc(kPage, kPool, kPage, 3 * kPage, 1, &b) == 0 && b == static_cast(6 * kPage)); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 3 * kPage, 1, &c) == 0 && c == 0); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 1)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_exhaustion_and_gap_reuse() { + int64_t a, b, c; + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(2 * kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == SCE_KERNEL_ERROR_EAGAIN); + DirectMemoryFree(static_cast(kPage), kPage); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(kPage)); + DirectMemoryFree(0, kPool); +} + +void free_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryFree(0, SIZE_MAX); + DirectMemoryFree(static_cast(kPool), kPage); + DirectMemoryFree(static_cast(kPool - kPage), 2 * kPage); + DirectMemoryFree(INT64_MAX, kPage); + CHECK(!DirectMemoryCheckedFree(0, SIZE_MAX)); + CHECK(!DirectMemoryCheckedFree(static_cast(kPool), kPage)); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryRetype(0, SIZE_MAX, 9); + DirectMemoryRetype(static_cast(kPool), kPage, 9); + DirectMemoryRetype(static_cast(kPool - kPage), 2 * kPage, 9); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); +} + +void free_partial_and_spanning() { + int64_t a, b; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 2, &b) == 0 && b == static_cast(4 * kPage)); + DirectMemoryFree(static_cast(2 * kPage), 4 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 1)); + CHECK(BlockIs(6 * kPage, 6 * kPage, 8 * kPage, 2)); + CHECK(DirectMemoryFreeRun(2 * kPage, kPool) == 4 * kPage); + CHECK(!DirectMemoryCheckedFree(0, 4 * kPage)); + CHECK(DirectMemoryCheckedFree(0, 2 * kPage)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_splits_and_skips_gaps() { + int64_t a; + CHECK(DirectMemoryAlloc(2 * kPage, kPool, 4 * kPage, 0, 1, &a) == 0 && a == static_cast(2 * kPage)); + DirectMemoryRetype(static_cast(3 * kPage), kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 1)); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 9)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 6 * kPage, 1)); + DirectMemoryRetype(0, 8 * kPage, 5); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 5)); + CHECK(BlockIs(5 * kPage, 4 * kPage, 6 * kPage, 5)); + int64_t s, e; + int t; + CHECK(!BlockAt(0, s, e, t) && !BlockAt(6 * kPage, s, e, t)); + DirectMemoryFree(0, kPool); +} + +void alloc_backing_bad_alloc_rolls_back() { + int64_t out = -1; + g_fault = Fault::BackingBadAlloc; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + g_fault = Fault::None; + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_node_bad_alloc_rolls_back() { + int64_t out = -1; + { + NewBudget budget(0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == SCE_KERNEL_ERROR_ENOMEM); + } + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_unexpected_exception_propagates() { + int64_t out = -1; + g_fault = Fault::BackingRuntimeError; + bool threw = false; + try { + (void)DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out); + } catch (const std::runtime_error&) { + threw = true; + } + g_fault = Fault::None; + CHECK(threw); + CHECK(PoolEmpty()); +} + +void free_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 0); + } + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 3)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 2 * kPage); + DirectMemoryFree(0, kPool); +} + +void retype_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + } + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 4 * kPage, 9)); + DirectMemoryFree(0, kPool); +} + +void forget_exception_propagates_and_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + g_fault = Fault::ForgetRuntimeError; + bool threwFree = false, threwChecked = false; + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwFree = true; + } + try { + (void)DirectMemoryCheckedFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwChecked = true; + } + g_fault = Fault::None; + CHECK(threwFree); + CHECK(threwChecked); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +struct Case { + const char* name; + void (*fn)(); +}; + +const Case kCases[] = { + {"alloc_unplaceable_requests", alloc_unplaceable_requests}, + {"alloc_null_out", alloc_null_out}, + {"alloc_rounding_alignment_and_clamping", alloc_rounding_alignment_and_clamping}, + {"alloc_non_power_of_two_alignment", alloc_non_power_of_two_alignment}, + {"alloc_exhaustion_and_gap_reuse", alloc_exhaustion_and_gap_reuse}, + {"free_out_of_range_is_ignored", free_out_of_range_is_ignored}, + {"retype_out_of_range_is_ignored", retype_out_of_range_is_ignored}, + {"free_partial_and_spanning", free_partial_and_spanning}, + {"retype_splits_and_skips_gaps", retype_splits_and_skips_gaps}, + {"alloc_backing_bad_alloc_rolls_back", alloc_backing_bad_alloc_rolls_back}, + {"alloc_node_bad_alloc_rolls_back", alloc_node_bad_alloc_rolls_back}, + {"alloc_unexpected_exception_propagates", alloc_unexpected_exception_propagates}, + {"free_node_bad_alloc_rolls_back", free_node_bad_alloc_rolls_back}, + {"retype_node_bad_alloc_rolls_back", retype_node_bad_alloc_rolls_back}, + {"forget_exception_propagates_and_rolls_back", forget_exception_propagates_and_rolls_back}, +}; + +} + +void CreateDirectMemoryBacking(int64_t, size_t, int) { + if (g_fault == Fault::BackingBadAlloc) throw std::bad_alloc(); + if (g_fault == Fault::BackingRuntimeError) throw std::runtime_error("backing"); +} + +void ForgetDirectMemory(int64_t, size_t) { + if (g_fault == Fault::ForgetRuntimeError) throw std::runtime_error("forget"); +} + +void* operator new(size_t n) { + if (g_newBudget == 0) throw std::bad_alloc(); + if (g_newBudget > 0) --g_newBudget; + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--list") == 0) { + for (const Case& c : kCases) std::printf("%s\n", c.name); + return 0; + } + int ran = 0; + for (const Case& c : kCases) { + if (argc == 2 && std::strcmp(argv[1], c.name) != 0) continue; + std::printf("[ RUN ] %s\n", c.name); + const int before = g_failures; + try { + c.fn(); + } catch (...) { + std::printf(" FAILED: unexpected exception escaped\n"); + ++g_failures; + } + std::printf("[ %s ] %s\n", g_failures == before ? " OK " : "FAIL", c.name); + ++ran; + } + if (ran == 0) { + std::printf("no such case\n"); + return 2; + } + return g_failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index de030efb03..386435ded6 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -18,6 +18,7 @@ void APS5_VABI sceLibcMspaceFree_nid_postfix(void*, void*); int APS5_VABI sceLibcMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t); std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void*); void* APS5_VABI sceLibcMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t); +void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::size_t); void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); @@ -187,6 +188,9 @@ int main() { void* memaligned = sceLibcMspaceMemalign_nid_postfix(arena, 256, 100); Require(memaligned && (reinterpret_cast(memaligned) & 255) == 0); Require(sceLibcMspaceMemalign_nid_postfix(arena, 24, 100) == nullptr); + void* alignedAlloc = sceLibcMspaceAlignedAlloc_nid_postfix(arena, 128, 256); + Require(alignedAlloc && (reinterpret_cast(alignedAlloc) & 127) == 0); + Require(sceLibcMspaceAlignedAlloc_nid_postfix(arena, 24, 100) == nullptr); unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); Require(realigned && (reinterpret_cast(realigned) & 63) == 0); for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index dd24b91013..719a0852ff 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -98,6 +98,28 @@ static void CheckNesting(const std::filesystem::path& path) { } } +static void CheckFlexibleMemorySize(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; + Require(!ParseText(path, prefix + "}").flexibleMemorySizeBytes, "Absent kernel changed the default budget"); + Require(!ParseText(path, prefix + R"(,"kernel":{}})").flexibleMemorySizeBytes, "Absent flexible size changed the default budget"); + for (const auto bytes : {std::uint64_t{16384}, std::uint64_t{838860800}, std::uint64_t{18446744073709535232ull}}) { + const auto parsed = ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + std::to_string(bytes) + "}}"); + Require(parsed.flexibleMemorySizeBytes == bytes, "Incorrect flexible memory size in bytes"); + } + for (const auto* value : {"0", "-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + value + "}}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, std::string("Accepted invalid flexible memory size: ") + value); + } + for (const auto* value : {"null", "true", "[]", "1", "\"kernel\""}) { + bool rejected = false; + try { ParseText(path, prefix + R"(,"kernel":)" + value + "}"); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "Accepted a non-object kernel"); + } +} + static void CheckLanguages(const std::filesystem::path& path) { const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); @@ -181,6 +203,7 @@ int main() { try { CheckNumericLocale(path); CheckUtf8(path); + CheckFlexibleMemorySize(path); CheckNumberSyntax(path); const std::pair sizes[] = { {"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096}, diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp new file mode 100644 index 0000000000..82d0ad962b --- /dev/null +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -0,0 +1,73 @@ +#pragma once +#include +#include +inline constexpr char RootPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIByDCCAW6gAwIBAgIDAIChMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBCMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMRwwGgYDVQQDDBNBbnlQUzUgZml4dHVy +ZSByb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGBewuI37COE1s3xt1y52 +kxoDW1vLl0IzSIlIjKyLrj80kjZWqXOR6X4Th8cyBPVeQkHh0CE+TjZtKpI3CL/H +6aNTMFEwHQYDVR0OBBYEFFZolbTcGqYVMtT6BD37g9yqaAzcMB8GA1UdIwQYMBaA +FFZolbTcGqYVMtT6BD37g9yqaAzcMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E +AwIDSAAwRQIgUQ4NhPYRVB5YgwemEZ5vm76nEY97hJYlq/GymdmmsLgCIQDKMTQC +eNbT9zlbdI+uo50utM9rDZ8kSBx5RsuAVj8M3w== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ClientPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIB3TCCAYOgAwIBAgIDEjRWMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDA4MjIxNDAyWhcNMzYxMDA1MjIxNDAyWjBoMQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMSIwDgYDVQQLDAdQcmltYXJ5MBAGA1UE +CwwJU2Vjb25kYXJ5MR4wHAYDVQQDDBVBbnlQUzUgZml4dHVyZSBjbGllbnQwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNEDnWt +ZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsXo0IwQDAdBgNV +HQ4EFgQUwVSZJzELeQ6acITiH929Z4kUuEUwHwYDVR0jBBgwFoAUVmiVtNwaphUy +1PoEPfuD3KpoDNwwCgYIKoZIzj0EAwIDSAAwRQIhAIJ3dzC+ddOvUbBXg4sCYTG3 +xXS9+jF9gxro9MqK1ZenAiA+l/Y8EMrvzjXx7kN5ZV+cVSnH1Ngn3kqaE8w7gIRK +NQ== +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char RootKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHIXi+x3Z7ozqaKSg +w4mBVS67OC+pzhTpjT6EhQ9/V32hRANCAAQYF7C4jfsI4TWzfG3XLnaTGgNbW8uX +QjNIiUiMrIuuPzSSNlapc5HpfhOHxzIE9V5CQeHQIT5ONm0qkjcIv8fp +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr char ClientKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHoANtFzCHilXfZPd +GmO1G+NKG5sSUnUWmDrU29+IaQahRANCAAR1Njvxhr/lzV/EnLlOEyir7zimMPNE +DnWtZYBape38P+jLkWNUgklWh0rIaqQq7rzISu0+mK+ee16bJC3qdWsX +-----END PRIVATE KEY----- +)FIXTURE"; +inline constexpr std::array RootDer{ + 0x30, 0x82, 0x01, 0xc8, 0x30, 0x82, 0x01, 0x6e, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x03, 0x00, + 0x80, 0xa1, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x42, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42, 0x31, 0x15, 0x30, + 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x54, + 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x13, 0x41, + 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x20, 0x72, 0x6f, + 0x6f, 0x74, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x36, 0x31, 0x30, 0x30, 0x38, 0x32, 0x32, 0x31, 0x34, + 0x30, 0x32, 0x5a, 0x17, 0x0d, 0x33, 0x36, 0x31, 0x30, 0x30, 0x35, 0x32, 0x32, 0x31, 0x34, 0x30, + 0x32, 0x5a, 0x30, 0x42, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, + 0x42, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0c, 0x41, 0x6e, 0x79, 0x50, + 0x53, 0x35, 0x20, 0x54, 0x65, 0x73, 0x74, 0x73, 0x31, 0x1c, 0x30, 0x1a, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x13, 0x41, 0x6e, 0x79, 0x50, 0x53, 0x35, 0x20, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, + 0x65, 0x20, 0x72, 0x6f, 0x6f, 0x74, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, + 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, + 0x04, 0x18, 0x17, 0xb0, 0xb8, 0x8d, 0xfb, 0x08, 0xe1, 0x35, 0xb3, 0x7c, 0x6d, 0xd7, 0x2e, 0x76, + 0x93, 0x1a, 0x03, 0x5b, 0x5b, 0xcb, 0x97, 0x42, 0x33, 0x48, 0x89, 0x48, 0x8c, 0xac, 0x8b, 0xae, + 0x3f, 0x34, 0x92, 0x36, 0x56, 0xa9, 0x73, 0x91, 0xe9, 0x7e, 0x13, 0x87, 0xc7, 0x32, 0x04, 0xf5, + 0x5e, 0x42, 0x41, 0xe1, 0xd0, 0x21, 0x3e, 0x4e, 0x36, 0x6d, 0x2a, 0x92, 0x37, 0x08, 0xbf, 0xc7, + 0xe9, 0xa3, 0x53, 0x30, 0x51, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, + 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, 0xdc, + 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, + 0x14, 0x56, 0x68, 0x95, 0xb4, 0xdc, 0x1a, 0xa6, 0x15, 0x32, 0xd4, 0xfa, 0x04, 0x3d, 0xfb, 0x83, + 0xdc, 0xaa, 0x68, 0x0c, 0xdc, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, + 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45, 0x02, 0x20, 0x51, 0x0e, 0x0d, 0x84, 0xf6, 0x11, 0x54, + 0x1e, 0x58, 0x83, 0x07, 0xa6, 0x11, 0x9e, 0x6f, 0x9b, 0xbe, 0xa7, 0x11, 0x8f, 0x7b, 0x84, 0x96, + 0x25, 0xab, 0xf1, 0xb2, 0x99, 0xd9, 0xa6, 0xb0, 0xb8, 0x02, 0x21, 0x00, 0xca, 0x31, 0x34, 0x02, + 0x78, 0xd6, 0xd3, 0xf7, 0x39, 0x5b, 0x74, 0x8f, 0xae, 0xa3, 0x9d, 0x2e, 0xb4, 0xcf, 0x6b, 0x0d, + 0x9f, 0x24, 0x48, 0x1c, 0x79, 0x46, 0xcb, 0x80, 0x56, 0x3f, 0x0c, 0xdf, +}; diff --git a/core/libs/tests/UndeliverableHostException.cpp b/core/libs/tests/UndeliverableHostException.cpp new file mode 100644 index 0000000000..6356af4e7b --- /dev/null +++ b/core/libs/tests/UndeliverableHostException.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +namespace { + +[[gnu::noinline]] void* AllocateWithoutUnwindData(std::size_t size) { + void* const block = ::operator new(size); + std::printf("allocated %p\n", block); + return block; +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + AllocateWithoutUnwindData(std::size_t{1} << 46); + std::fputs("operator new returned instead of throwing\n", stderr); + return 1; +} diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 08b6d4ea12..b44c48af61 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -172,6 +172,36 @@ void CheckPrivate() { VirtualFree(memory, 0, MEM_RELEASE); } +void CheckForgottenSectionKeepsNoPages() { + auto& mappings = Mappings::Get(); + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Bytes, nullptr); + auto* first = static_cast(mappings.Reserve(nullptr, Bytes)); + auto* second = static_cast(mappings.Reserve(nullptr, Bytes)); + Require(section && first && second, "cannot allocate forgotten section test memory"); + const auto resident = [&](unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "resident query failed"); + return info.State == MEM_COMMIT && info.Type == MEM_PRIVATE; + }; + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + mappings.Map(second, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(!resident(first) && !resident(second), "a second mapping kept the first resident"); + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Reset(second, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first), "a forgotten section's handle value kept its pages in the section"); + first[0] = 7; + mappings.ForgetSection(section); + mappings.Reset(first, Bytes); + mappings.Map(first, Bytes, section, 0, PAGE_READWRITE, true, true); + Require(resident(first) && first[0] == 0, "a forgotten section's handle value kept its unmapped bytes"); + mappings.Reset(first, Bytes); + VirtualFree(first, 0, MEM_RELEASE); + VirtualFree(second, 0, MEM_RELEASE); + CloseHandle(section); +} + void CheckConcurrentWrites() { Region region; std::atomic start{false}; @@ -199,6 +229,7 @@ int main() { CheckFailureAndHoles(); CheckPrivate(); CheckConcurrentWrites(); + CheckForgottenSectionKeepsNoPages(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fcbc225d72..b5a2a2b685 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -55,6 +55,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CodeInstructionCollector.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SceCodeExtent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -117,8 +118,9 @@ if(BUILD_TESTING AND WIN32) endif() if(WIN32) - target_sources(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc) + add_library(relinker_resources OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc) set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/relinker.rc PROPERTIES OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/relinker.manifest) + target_sources(relinker PRIVATE $) target_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) endif() @@ -394,6 +396,9 @@ if(BUILD_TESTING) set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) + add_test(NAME conversion_diagnostics COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_conversion_diagnostics.py $) + set_tests_properties(conversion_diagnostics PROPERTIES TIMEOUT 20) add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE} @@ -433,8 +438,12 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executable_segments.py $) add_test(NAME linux_executable_exports COMMAND ${Python3_EXECUTABLE} ${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 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} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_fixed_base.py $) add_test(NAME linux_program_header_order COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_program_header_order.py $) add_test(NAME linux_input_program_headers COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index a2a4364f1b..68c5c86753 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -26,6 +26,7 @@ struct Args { bool windowsGui = false; std::optional windowsFixedBase; bool traceImports = false; + std::optional linuxFixedBase; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 6800226191..7e809c530f 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -1,19 +1,33 @@ #include #include -#include #include +#include #include #include #include #include +#include namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base
] [--linux-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [--frame-red-zone] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } +static std::uint64_t ParseLinuxFixedBase(std::string_view value) { + constexpr std::uint64_t lowest = 0x10000; + constexpr std::uint64_t highest = 0x7fffffff0000; + constexpr std::uint64_t alignment = 0x10000; + const bool hexadecimal = value.starts_with("0x") || value.starts_with("0X"); + if (hexadecimal) value.remove_prefix(2); + std::uint64_t address = 0; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10); + if (value.empty() || parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < lowest || address > highest || address % alignment != 0) + throw std::runtime_error("--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"); + return address; +} + Args ParseArgs(int argc, char* argv[]) { Args args; bool unusedFilterSpecified = false; @@ -35,6 +49,11 @@ Args ParseArgs(int argc, char* argv[]) { if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < 0x20000 || address > 0x7fffffff0000 || (address & 0xffff) != 0) throw std::runtime_error("--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); args.windowsFixedBase = address; + } else if (arg == "--linux-fixed-base") { + if (args.linuxFixedBase) throw std::runtime_error("--linux-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string_view(argv[i + 1]).empty() || std::string_view(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--linux-fixed-base requires an address"); + args.linuxFixedBase = ParseLinuxFixedBase(argv[++i]); } else if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; } else if (arg == "--trap-sdk-syscall-stubs") { @@ -132,6 +151,10 @@ Args ParseArgs(int argc, char* argv[]) { throw std::runtime_error("--trace-imports requires --windows"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); + if (args.linuxFixedBase && (args.toWindows || args.toMacos)) + throw std::runtime_error("--linux-fixed-base cannot be used with --windows or --macos"); + if (args.linuxFixedBase && args.lazyBinding) + throw std::runtime_error("--linux-fixed-base conflicts with --lazy-binding"); if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty())) throw std::runtime_error(Usage()); diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 4331181592..6d0bd857a3 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -222,6 +222,60 @@ void windowsFixedBase() { expectThrows({"--windows", "--windows-fixed-base", "0x400000", "--windows-fixed-base", "0x800000", "input.elf", "output.exe"}, "--windows-fixed-base cannot be repeated"); } +void linuxFixedBase() { + require(!parse({"input.elf", "output.elf"}).linuxFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) + require(parse({"--linux-fixed-base", value, "input.elf", "output.elf"}).linuxFixedBase == 0x400000, "fixed base was parsed incorrectly"); + require(parse({"input.elf", "--linux-fixed-base", "0x10000", "output.elf"}).linuxFixedBase == 0x10000, "lowest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x7fffffff0000", "input.elf", "output.elf"}).linuxFixedBase == 0x7fffffff0000, "highest fixed base was rejected"); + require(parse({"--linux-fixed-base", "0x400000000", "input.elf", "output.elf"}).linuxFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + const std::string range = "--linux-fixed-base requires a 64 KiB-aligned address from 0x10000 through 0x7fffffff0000"; + for (const auto& value : {"0", "0x0", "0xf000", "0x400001", "0x408000", "0x", "0X", "-4194304", "+4194304", " 4194304", "4194304 ", "0x400000junk", "0x-400000", "18446744073709551616", "0x800000000000", "0x7fffffff10000"}) + expectThrows({"--linux-fixed-base", value, "input.elf", "output.elf"}, range); + expectThrows({"--linux-fixed-base"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "--windows", "input.elf", "output.elf"}, "--linux-fixed-base requires an address"); + expectThrows({"--linux-fixed-base", "0x400000", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base cannot be repeated"); + expectThrows({"--windows", "--linux-fixed-base", "0x400000", "input.elf", "output.exe"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--linux-fixed-base", "0x400000", "--macos", "input.elf", "output"}, "--linux-fixed-base cannot be used with --windows or --macos"); + expectThrows({"--lazy-binding", "--linux-fixed-base", "0x400000", "input.elf", "output.elf"}, "--linux-fixed-base conflicts with --lazy-binding"); +} + +} + +void moduleDirectories() { + const auto args = parse({"--module-dir", "Media/Plugins", "input.elf", "--module-dir", "extra", "output.elf"}); + require(args.moduleDirectories == std::vector{"Media/Plugins", "extra"}, "--module-dir directories were not recorded in order"); + require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "--module-dir consumed a positional path"); + expectThrows({"--module-dir"}, "--module-dir requires a directory"); + expectThrows({"--skip-sce-module", "--module-dir", "Media/Plugins", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); + expectThrows({"--module-dir", "Media/Plugins", "--skip-sce-module", "input.elf", "output.elf"}, "--module-dir conflicts with --skip-sce-module"); +} + +void sceModulePath() { + const auto args = parse({"--sce-module-path", "modules", "input.elf", "output.elf"}); + require(args.sceModulePath == "modules", "--sce-module-path value was not captured"); + expectThrows({"--sce-module-path"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "--registry", "input.elf", "output.elf"}, "--sce-module-path requires a nonempty path"); + expectThrows({"--sce-module-path", "a", "--sce-module-path", "b", "input.elf", "output.elf"}, "--sce-module-path must be specified once"); + expectThrows({"--skip-sce-module", "--sce-module-path", "modules", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); + expectThrows({"--sce-module-path", "modules", "--skip-sce-module", "input.elf", "output.elf"}, "--sce-module-path conflicts with --skip-sce-module"); +} + +void targetSelection() { + const auto macos = parse({"--macos", "input.elf", "output.elf"}); + require(macos.toMacos && !macos.toWindows, "--macos did not select only the macOS target"); + const auto rosetta = parse({"--to-rosetta", "input.elf", "output.elf"}); + require(rosetta.toRosetta && rosetta.toIntel, "--to-rosetta did not imply --to-intel"); + const auto rosettaOnMacos = parse({"--macos", "--to-rosetta", "input.elf", "output.elf"}); + require(rosettaOnMacos.toMacos && rosettaOnMacos.toRosetta && rosettaOnMacos.toIntel, "--to-rosetta was rejected with --macos"); + const std::string targetConflict = "--windows conflicts with --macos"; + expectThrows({"--windows", "--macos", "input.elf", "output.elf"}, targetConflict); + expectThrows({"--macos", "--windows", "input.elf", "output.elf"}, targetConflict); + const std::string rosettaConflict = "--to-rosetta conflicts with --windows"; + expectThrows({"--windows", "--to-rosetta", "input.elf", "output.elf"}, rosettaConflict); + expectThrows({"--to-rosetta", "--windows", "input.elf", "output.elf"}, rosettaConflict); } int main() { @@ -238,6 +292,9 @@ int main() { excludeSceModuleConsumesNextTokenVerbatim(); excludeSceModuleWithoutValue(); excludeConflictsWithSkip(); + moduleDirectories(); + sceModulePath(); + targetSelection(); executables(); unusedFilter(); unusedFilterRejectsBadValues(); @@ -250,6 +307,7 @@ int main() { windowsGuiRequiresWindows(); missingPositionals(); windowsFixedBase(); + linuxFixedBase(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index dc0099f1d1..95099d7477 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -58,17 +58,20 @@ inline constexpr std::uint64_t kDynamicHeaderAlignment = 8; inline constexpr std::uint64_t kInterpHeaderAlignment = 1; inline constexpr std::uint16_t kSyntheticProgramHeaderCount = 5; +inline constexpr std::uint32_t PT_NULL = 0; inline constexpr std::uint32_t PT_LOAD = 1; inline constexpr std::uint32_t PT_DYNAMIC = 2; inline constexpr std::uint32_t PT_INTERP = 3; inline constexpr std::uint32_t PT_NOTE = 4; inline constexpr std::uint32_t PT_PHDR = 6; +inline constexpr std::uint32_t PT_TLS = 7; inline constexpr std::uint32_t PT_GNU_EH_FRAME = 0x6474e550; inline constexpr std::uint32_t PT_GNU_STACK = 0x6474e551; inline constexpr std::uint32_t PT_GNU_RELRO = 0x6474e552; inline constexpr std::uint32_t PF_X = 0x1; inline constexpr std::uint32_t PF_W = 0x2; inline constexpr std::uint32_t PF_R = 0x4; +inline constexpr std::uint32_t ET_EXEC = 2; inline constexpr std::uint32_t ET_DYN = 3; inline constexpr std::uint32_t PT_SCE_DYNLIBDATA = 0x61000000; inline constexpr std::uint32_t PT_OS_PROCPARAM = 0x61000001; @@ -137,15 +140,22 @@ inline constexpr std::uint32_t R_X86_64_RELATIVE = 8; inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9; inline constexpr std::uint32_t R_X86_64_32 = 10; inline constexpr std::uint32_t R_X86_64_32S = 11; +inline constexpr std::uint32_t R_X86_64_DTPMOD64 = 16; +inline constexpr std::uint32_t R_X86_64_DTPOFF64 = 17; +inline constexpr std::uint32_t R_X86_64_TPOFF64 = 18; inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; +inline constexpr std::uint32_t R_X86_64_IRELATIVE = 37; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; +inline constexpr std::uint8_t STT_TLS = 6; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; +inline constexpr std::uint16_t SHN_LORESERVE = 0xff00; +inline constexpr std::uint16_t SHN_ABS = 0xfff1; inline constexpr std::uint32_t SHT_NULL = 0; inline constexpr std::uint32_t SHT_PROGBITS = 1; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index ae5782f330..19719a7b0f 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include namespace Elfpatcher::Linux { @@ -18,7 +20,8 @@ public: std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase = std::nullopt ); std::vector Patch( @@ -37,6 +40,7 @@ private: std::shared_ptr _programHeaderLayoutBuilder; std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; + std::optional _fixedBase; 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/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index 9c0b8f38f6..db44e19058 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -8,7 +8,7 @@ namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt, bool dynamicBase = true) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp index 0d641e8fed..c9d0c656e3 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsRelocationBuilder.hpp @@ -7,7 +7,8 @@ namespace Elfpatcher::Windows { class WindowsRelocationBuilder { public: - PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const; + PeRelocations Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const; std::vector BuildBaseRelocations(const std::vector& targets) const; }; diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 4fd051eba5..60d7c8b690 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -8,20 +8,124 @@ #include #include #include +#include #include namespace Elfpatcher::Linux { +namespace { + +constexpr std::uint64_t kLinuxUserAddressEnd = 0x800000000000; + +std::string Hex(const std::uint64_t value) { + std::ostringstream text; + text << "0x" << std::hex << value; + return text.str(); +} + +std::uint64_t Rebased(const std::uint64_t base, const std::uint64_t address, const std::string& what) { + if (base > kLinuxUserAddressEnd || address > kLinuxUserAddressEnd - base) + throw Domain::RelinkerException(what + " " + Hex(address) + " lies outside the Linux user address space at fixed base " + Hex(base)); + return base + address; +} + +void RequireWholeEntries(const std::vector& table, const std::uint64_t entrySize, const std::string& what) { + if (table.size() % entrySize != 0) + throw Domain::RelinkerException(what + " is not a whole number of entries"); +} + +void RequireOnlyDependencies(const std::vector& entries) { + RequireWholeEntries(entries, kDynEntrySize, "Executable dependency table"); + for (std::size_t entry = 0; entry < entries.size(); entry += kDynEntrySize) { + const auto tag = static_cast(Io::ReadU64(entries, entry)); + if (tag != DT_NEEDED) + throw Domain::RelinkerException("Dynamic tag " + Hex(static_cast(tag)) + " cannot be placed at a fixed Linux base"); + } +} + +std::vector RebasedSymbols(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kSymEntrySize, "Dynamic symbol table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kSymEntrySize) { + const auto section = Io::ReadU16(rebased, entry + 6); + if (section == SHN_UNDEF || section == SHN_ABS || (rebased[entry + 4] & 0xf) == STT_TLS) + continue; + if (section >= SHN_LORESERVE) + throw Domain::RelinkerException("Symbol section index " + Hex(section) + " cannot be placed at a fixed Linux base"); + Io::WriteU64(rebased, entry + 8, Rebased(base, Io::ReadU64(rebased, entry + 8), "Symbol")); + } + return rebased; +} + +std::vector RebasedRelocations(const std::vector& table, const std::uint64_t base) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + auto rebased = table; + for (std::size_t entry = 0; entry < rebased.size(); entry += kRelaEntrySize) { + const auto type = static_cast(Io::ReadU64(rebased, entry + 8)); + switch (type) { + case R_X86_64_NONE: + continue; + case R_X86_64_RELATIVE: + case R_X86_64_IRELATIVE: { + const auto addend = static_cast(Io::ReadU64(rebased, entry + 16)); + if (addend < 0) + throw Domain::RelinkerException("Relocation target " + std::to_string(addend) + " lies below the image at a fixed Linux base"); + Io::WriteU64(rebased, entry + 16, Rebased(base, static_cast(addend), "Relocation target")); + break; + } + case R_X86_64_64: + case R_X86_64_PC32: + case R_X86_64_COPY: + case R_X86_64_GLOB_DAT: + case R_X86_64_JUMP_SLOT: + case R_X86_64_32: + case R_X86_64_32S: + case R_X86_64_DTPMOD64: + case R_X86_64_DTPOFF64: + case R_X86_64_TPOFF64: + break; + default: + throw Domain::RelinkerException("Relocation type " + std::to_string(type) + " cannot be placed at a fixed Linux base"); + } + Io::WriteU64(rebased, entry, Rebased(base, Io::ReadU64(rebased, entry), "Relocation offset")); + } + return rebased; +} + +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); + const auto type = Io::ReadU32(buf, header + kPhdrTypeOffset); + if (type == PT_NULL || type == PT_GNU_STACK) + continue; + const auto address = Rebased(base, Io::ReadU64(buf, header + kPhdrVaddrOffset), "Program header address"); + const auto physical = Rebased(base, Io::ReadU64(buf, header + kPhdrPaddrOffset), "Program header physical address"); + if (type == PT_LOAD) { + const auto alignment = Io::ReadU64(buf, header + kPhdrAlignOffset); + if (alignment > 1 && base % alignment != 0) + throw Domain::RelinkerException("Fixed Linux base " + Hex(base) + " is not a multiple of the PT_LOAD alignment " + Hex(alignment)); + if (Io::ReadU64(buf, header + kPhdrMemSizeOffset) > kLinuxUserAddressEnd - address) + throw Domain::RelinkerException("PT_LOAD at " + Hex(address) + " ends outside the Linux user address space"); + } + Io::WriteU64(buf, header + kPhdrVaddrOffset, address); + Io::WriteU64(buf, header + kPhdrPaddrOffset, physical); + } +} + +} + LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr entryStubBuilder, std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, - std::shared_ptr byteWriter + std::shared_ptr byteWriter, + std::optional fixedBase ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) + , _fixedBase(fixedBase) { } @@ -78,13 +182,26 @@ std::vector LinuxElfPatcher::Patch( { if (dependencyDiagnostics) 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 (_fixedBase) + RequireOnlyDependencies(dynSection.DynamicSegmentData); + 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; + }; + const auto table = [this, base](const std::vector& data, const bool relocations) { + if (!_fixedBase) return data; + return relocations ? RebasedRelocations(data, base) : RebasedSymbols(data, base); + }; std::vector buf = sourceElf; + const std::uint32_t type = _fixedBase ? ET_EXEC : ET_DYN; buf[kEhdrOsAbiOffset] = 0; buf[kEhdrAbiVersionOffset] = 0; - buf[kEhdrTypeOffset] = static_cast(ET_DYN & 0xFF); - buf[kEhdrTypeOffset + 1] = static_cast((ET_DYN >> 8) & 0xFF); + buf[kEhdrTypeOffset] = static_cast(type & 0xFF); + buf[kEhdrTypeOffset + 1] = static_cast((type >> 8) & 0xFF); _byteWriter->WriteU64(buf, kEhdrShOffOffset, 0); _byteWriter->WriteU16(buf, kEhdrShNumOffset, 0); _byteWriter->WriteU16(buf, kEhdrShStrNdxOffset, 0); @@ -109,7 +226,8 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.DynSymData) + const auto symbols = table(dynSection.DynSymData, false); + for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); @@ -123,12 +241,14 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaData) + const auto relocations = table(dynSection.RelaData, true); + for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); const auto relaPltOff = static_cast(buf.size()); - for (std::uint8_t b : dynSection.RelaPltData) + const auto pltRelocations = table(dynSection.RelaPltData, true); + for (std::uint8_t b : pltRelocations) buf.push_back(b); alignBuf(buf, kRelaPltAlignment); @@ -144,22 +264,22 @@ std::vector LinuxElfPatcher::Patch( std::vector dynSegBuf; for (std::uint8_t b : dynSection.DynamicSegmentData) dynSegBuf.push_back(b); - _appendDynEntry(dynSegBuf, DT_STRTAB, vaddrOfExtraBlockOffset(dynStrOff)); + _appendDynEntry(dynSegBuf, DT_STRTAB, address(vaddrOfExtraBlockOffset(dynStrOff))); _appendDynEntry(dynSegBuf, DT_STRSZ, dynStrSize); - _appendDynEntry(dynSegBuf, DT_SYMTAB, vaddrOfExtraBlockOffset(dynSymOff)); + _appendDynEntry(dynSegBuf, DT_SYMTAB, address(vaddrOfExtraBlockOffset(dynSymOff))); _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); - _appendDynEntry(dynSegBuf, DT_HASH, vaddrOfExtraBlockOffset(hashOff)); + _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); if (!dynSection.RelaData.empty()) { - _appendDynEntry(dynSegBuf, DT_RELA, vaddrOfExtraBlockOffset(relaOff)); + _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { - _appendDynEntry(dynSegBuf, DT_JMPREL, vaddrOfExtraBlockOffset(relaPltOff)); + _appendDynEntry(dynSegBuf, DT_JMPREL, address(vaddrOfExtraBlockOffset(relaPltOff))); _appendDynEntry(dynSegBuf, DT_PLTRELSZ, dynSection.RelaPltData.size()); _appendDynEntry(dynSegBuf, DT_PLTREL, static_cast(DT_RELA)); - _appendDynEntry(dynSegBuf, DT_PLTGOT, originalPltGotVaddr); + _appendDynEntry(dynSegBuf, DT_PLTGOT, address(originalPltGotVaddr)); } if (!lazyBinding) _appendDynEntry(dynSegBuf, DT_FLAGS, DF_BIND_NOW); @@ -173,10 +293,10 @@ std::vector LinuxElfPatcher::Patch( 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(stubVaddr, realEntryVaddr); + const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); for (std::uint8_t b : stubBytes) buf.push_back(b); - _byteWriter->WriteU64(buf, kEhdrEntryOffset, stubVaddr); + _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) _appendTrampoline(buf, site, extraBlockOff, vaddrOfExtraBlockOffset); @@ -214,6 +334,8 @@ std::vector LinuxElfPatcher::Patch( const std::uint16_t writtenPh = _programHeaderLayoutBuilder->WriteLayout(buf, layoutRequest); _byteWriter->WriteU16(buf, kEhdrPhNumOffset, writtenPh); + if (_fixedBase) + RebaseProgramHeaders(buf, phOff, phEntSize, writtenPh, base); SectionHeaderTableRequest sectionRequest{}; sectionRequest.DynStrOffset = dynStrOff; diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index ff7e31f79a..b8c3f7b06f 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -114,7 +115,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma try { return WriteMacOsImage(input); } catch (Domain::RelinkerException& error) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } } diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 8e76037730..2a56daf6dc 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -46,7 +46,7 @@ std::vector MacOsMachOPatcher::Patch(const std::vector not found; nothing is imported from it, continuing without it\n"); const auto errorPrefix = addString("GetLastError: "); + const auto moduleNotFoundHint = addString("hint: the module or one of its dependent DLLs was not found; a self-built libs/ also needs libgcc_s_seh-1.dll, libstdc++-6.dll and libwinpthread-1.dll from the toolchain's mingw64/bin next to it\n"); const auto messageSeparator = addString(" - "); const auto newline = addString("\n"); const auto searched = addString("Searched libraries:\n"); @@ -486,6 +487,11 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeString(resolvedPaths[index], true); writeString(newline, true); writeLastError(); + code.Rip({0x8b, 0x05}, lastError); + code.Emit({0x83, 0xf8, 0x7e}); + const auto notModuleNotFound = code.Branch({0x0f, 0x85}); + writeString(moduleNotFoundHint, true); + code.PatchBranch(notModuleNotFound, code.GetRva()); if (dependencyDiagnostics) { code.Rip({0x48, 0x8d, 0x0d}, resolvedPaths[index]); code.Rip({0x48, 0x8d, 0x15}, programText); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 2e621c7cb0..ded45f6d76 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -58,8 +59,8 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else if (type == 18) { const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); if (tls == guest.Headers.end() || (symbolIndex != 0 && (symbol.Section == 0 || (symbol.Info & 15) != 6))) throw Domain::RelinkerException("Unsupported external static TLS relocation", target); - const auto size = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 16)); - image.WritePointer(target, symbol.Value + addend - size); + const auto threadPointerOffset = Io::AlignUp64(tls->MemorySize, std::max(tls->Alignment, 1)); + image.WritePointer(target, symbol.Value + addend - threadPointerOffset); } else if (type == 1 || type == 6 || type == 7) { if (symbolIndex == 0 || (type != 1 && addend != 0) || (symbol.Info & 15) == 6) throw Domain::RelinkerException("Invalid guest symbol relocation", target); if (symbol.Section == Relinker::AbsoluteSection) image.WritePointer(target, symbol.Value + addend); @@ -84,7 +85,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI try { directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); } catch (Domain::RelinkerException& error) { - error.InputPath = guest.SourcePath.string(); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; } WindowsTrampolineBuilder().Build(guest.Trampolines, image, sections, nextRva); diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 2b4faa6bf5..c506a7a1a8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -44,7 +44,7 @@ std::vector WindowsPePatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector(image.GetImageBase()) : std::nullopt); + auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional(image.GetImageBase()) : std::nullopt, false); report(10000); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index 1e346dbeda..0b64a5a2d5 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase) const { +std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase, const bool dynamicBase) const { constexpr std::size_t peOffset = 0x80; constexpr std::size_t optionalOffset = peOffset + 24; constexpr std::size_t sectionTable = optionalOffset + 240; @@ -32,7 +32,7 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, optionalOffset + 50, 2); Io::WriteU32(result, optionalOffset + 60, LoadRva); Io::WriteU16(result, optionalOffset + 68, windowsGui ? 2 : 3); - Io::WriteU16(result, optionalOffset + 70, directories[5].Size == 0 ? 0x100 : 0x140); + Io::WriteU16(result, optionalOffset + 70, dynamicBase && directories[5].Size != 0 ? 0x140 : 0x100); Io::WriteU64(result, optionalOffset + 72, 0x210000); Io::WriteU64(result, optionalOffset + 80, 0x200000); Io::WriteU64(result, optionalOffset + 88, 0x100000); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index f1b5036de8..e7cd5aec37 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -1,11 +1,15 @@ #include #include #include +#include +#include #include +#include namespace Elfpatcher::Windows { -PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection) const { +PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Domain::SysVDynamicSection& dynamicSection, + const std::vector& headers) const { if (dynamicSection.DynSymData.empty() || dynamicSection.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid ELF dynamic symbol table size"); PeRelocations result; @@ -19,7 +23,7 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom const auto addend = Io::ReadU64(table, offset + 16); const auto type = static_cast(info); const auto symbol = info >> 32; - if ((plt && type != 7) || (type != 1 && type != 6 && type != 7 && type != 8)) + 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); const auto rva = image.GetRva(target, 8); const auto next = ranges.lower_bound(target); @@ -33,6 +37,21 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom result.BaseRelocations.push_back(rva); continue; } + if (type == 16 || type == 17 || type == 18) { + if (symbol != 0) + throw Domain::RelinkerException("Imported TLS relocation " + std::to_string(type) + " is not supported for this target", target); + if (type == 16) + throw Domain::RelinkerException("Executable DTPMOD64 relocation is not supported for this target", target); + const auto tls = std::find_if(headers.begin(), headers.end(), [](const auto& header) { return header.Type == 7; }); + if (tls == headers.end() || tls->MemorySize == 0) + throw Domain::RelinkerException("TLS relocation without a non-empty PT_TLS segment", target); + const auto alignment = std::max(tls->Alignment, 1); + if (!std::has_single_bit(alignment) || tls->MemorySize > 0x7fff0000u) + throw Domain::RelinkerException("Invalid or unsupported ELF TLS layout", tls->Offset); + image.RequireWritable(target, 8); + image.WritePointer(target, type == 17 ? addend : addend - Io::AlignUp64(tls->MemorySize, alignment)); + continue; + } if (symbol == 0 || symbol >= dynamicSection.DynSymData.size() / 24) throw Domain::RelinkerException("Invalid import symbol index", target); const auto symbolOffset = static_cast(symbol) * 24; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0398c6c3bb..991107a0a7 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -61,6 +61,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, options.TraceImports = args.traceImports; options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; + options.LinuxFixedBase = args.linuxFixedBase; options.ExcludedModules = args.excludedSceModules; progress.IncludeFile(Io::Utf8Path(input.InputPath)); progress.IncludeFile(Io::Utf8Path(input.OutputPath)); @@ -92,6 +93,8 @@ int Run(const int argc, char* argv[]) { if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output would overwrite the input executable"); + if (Io::NativePath(args.outputPath).filename().empty() || std::filesystem::is_directory(Io::NativePath(args.outputPath))) + throw Domain::RelinkerException("Executable output is a directory: " + args.outputPath); const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); struct ExtraExecutable { diff --git a/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp new file mode 100644 index 0000000000..c43fd18cd6 --- /dev/null +++ b/core/relinker/relinker/include/relinker/analysis/SceCodeExtent.hpp @@ -0,0 +1,14 @@ +#ifndef RELINKER_ANALYSIS_SCECODEEXTENT_HPP +#define RELINKER_ANALYSIS_SCECODEEXTENT_HPP + +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize); + +} + +#endif diff --git a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp index 98bb86c9de..f2c5ea47a7 100644 --- a/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp +++ b/core/relinker/relinker/include/relinker/domain/IUnusedNidFilter.hpp @@ -14,7 +14,8 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries = {} ) = 0; }; diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index 9840e93eca..cd7db5edda 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -22,6 +22,7 @@ struct ConversionOptions { bool TraceImports = false; std::optional WindowsFixedBase; bool WindowsDiagnostics = false; + std::optional LinuxFixedBase; std::string RunPath = "$ORIGIN/libs"; std::set ExcludedModules; }; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index fbd29c405f..120e7abac6 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -79,6 +79,8 @@ private: std::vector CallSiteResolver::ResolveCallSites(const std::vector& textSection, const FileByteOffset textSectionVAddr, const VirtualAddress targetGotOrPltAddress, const ByteCount targetGotOrPltSize) { 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(); @@ -99,7 +101,7 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector if (dispOff + 4 > textSection.size()) continue; std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); - VirtualAddress target = static_cast(static_cast(instrEnd) + disp); + VirtualAddress target = instrEnd + static_cast(static_cast(disp)); _cachedTargetToInstr.emplace_back(target, m.Offset); } std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 2fd1a9a2eb..c5ffda7b73 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -53,8 +54,9 @@ std::set CodeInstructionCollector::Collect(const std::ve } throw Domain::RelinkerException("Code analysis: unmapped address", address); }; + auto codeHeaders = headers; const auto isCode = [&](std::uint64_t address) { - for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; + for (const auto& header : codeHeaders) if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) return true; return false; }; std::map tags; @@ -91,6 +93,21 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + if (sceBase) { + const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + 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); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + } + } std::set roots; std::map functions; const auto addRoot = [&](std::uint64_t address) { if (isCode(address)) roots.insert(address); }; @@ -344,11 +361,11 @@ std::set CodeInstructionCollector::Collect(const std::ve } std::size_t previousRoots = 0; std::unordered_set decodedInstructions; - std::vector> sectionReachable(headers.size()); + std::vector> sectionReachable(codeHeaders.size()); do { previousRoots = roots.size(); - for (std::size_t i = 0; i < headers.size(); ++i) { - const auto& header = headers[i]; + for (std::size_t i = 0; i < codeHeaders.size(); ++i) { + const auto& header = codeHeaders[i]; if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize == 0) continue; range(header.Offset, header.FileSize); std::vector entries; diff --git a/core/relinker/relinker/src/analysis/SceCodeExtent.cpp b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp new file mode 100644 index 0000000000..106528e542 --- /dev/null +++ b/core/relinker/relinker/src/analysis/SceCodeExtent.cpp @@ -0,0 +1,40 @@ +#include +#include +#include + +namespace Relinker { + +std::optional FindSceCodeEnd(std::span segment, VirtualAddress segmentVAddr, const std::vector& functions, VirtualAddress pltGot, ByteCount pltRelocationsSize) { + VirtualAddress codeEnd = 0; + for (const auto& function : functions) + if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) + codeEnd = std::max(codeEnd, function.End); + if (codeEnd == 0) + return std::nullopt; + if (codeEnd - segmentVAddr > segment.size()) + throw RelinkerException("Unwind function exceeds executable segment", codeEnd); + if (pltRelocationsSize == 0) + return codeEnd; + const auto target = [&](const std::size_t at) { + std::int32_t displacement = 0; + std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); + return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); + }; + const std::size_t lastFunctionEnd = codeEnd - segmentVAddr; + std::optional plt; + for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { + if (segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 + && target(at) == pltGot + 8 && target(at + 6) == pltGot + 16) { + plt = at; + break; + } + } + if (!plt) + throw RelinkerException("PLT not found after the last unwind function", codeEnd); + const ByteCount pltSize = 16 * (1 + pltRelocationsSize / 24); + if (pltSize > segment.size() - *plt) + throw RelinkerException("PLT exceeds executable segment", segmentVAddr + *plt); + return segmentVAddr + *plt + pltSize; +} + +} diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp index 1426881a92..3dbe4ab5c4 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter.cpp @@ -13,13 +13,16 @@ public: const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, - VirtualAddress textVAddr + VirtualAddress textVAddr, + const std::vector& extraEntries ) override { if (textSection.empty()) throw RelinkerException("Cannot filter NIDs: text section is empty"); auto relativeRelocations = UnusedNidFilter::BuildRelativeRelocationIndex(elfBytes); auto collector = UnusedNidFilter::MakeEntryPointCollector(); auto entries = collector->Collect(elfBytes, textVAddr, textSection.size(), *relativeRelocations); + for (const auto entry : extraEntries) + if (entry >= textVAddr && entry - textVAddr < textSection.size()) entries.push_back(entry); if (entries.empty()) throw RelinkerException("Cannot filter NIDs: no entry points found"); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 8ce7c297b3..fd61157b6a 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -238,7 +238,12 @@ private: encoded(position, callEncoding); const auto landing = encoded(position, callEncoding); leb(position); - if (landing != 0) region.ExtraTargets.push_back(landingBase + landing); + if (landing != 0) { + const bool negative = (callEncoding & 0x08) != 0 && (landing >> 63) != 0; + if (negative ? landingBase < std::uint64_t{0} - landing : landing > std::numeric_limits::max() - landingBase) + throw RelinkerException("Strict filter: LSDA landing pad address overflowed", address); + region.ExtraTargets.push_back(landingBase + landing); + } } if (position != tableEnd) throw RelinkerException("Strict filter: LSDA call-site table size mismatch", address); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index 8e87a10d8e..e95afa383b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -75,11 +76,9 @@ public: if (phEntSize < 56) throw RelinkerException("ELF program header entry too small"); std::vector loads; - for (std::uint16_t i = 0; i < phCount; ++i) { - std::size_t phPos = static_cast(phOff) + i * phEntSize; - if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); - if (read32(elfBytes, phPos) != PT_LOAD) continue; - loads.push_back({read64(elfBytes, phPos + 16), read64(elfBytes, phPos + 8), read64(elfBytes, phPos + 32)}); + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } for (std::uint16_t i = 0; i < phCount; ++i) { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 39de6fdf66..1d0be749bc 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -147,12 +148,10 @@ std::unique_ptr BuildRelativeRelocationIndex( hasSceDynlibData = true; sceTableBase = read64(elfBytes, phPos + 8); } - if (type != PT_LOAD) continue; - LoadSegment seg; - seg.fileOffset = read64(elfBytes, phPos + 8); - seg.vaddr = read64(elfBytes, phPos + 16); - seg.fileSize = read64(elfBytes, phPos + 32); - loads.push_back(seg); + } + for (const auto& header : ElfReader(elfBytes).ReadProgramHeaders()) { + if (header.Type != PT_LOAD) continue; + loads.push_back({header.MappedAddress, header.Offset, header.FileSize}); } std::unordered_map targets; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 2fb2cc1158..c2a24c806f 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -192,7 +192,7 @@ class StrictNidFilter : public IUnusedNidFilter { public: explicit StrictNidFilter(Domain::IProgress& progress) : progress(progress) {} - std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr) override { + std::vector Filter(const std::vector& nidRefs, const std::vector& elfBytes, const std::vector& textSection, VirtualAddress textVAddr, const std::vector&) override { const StrictImage image(elfBytes); const auto input = image.Build(nidRefs, textSection, textVAddr); const auto analysis = UnusedNidFilter::AnalyzeStrictReachability(input); diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 2f2f4f91bc..6cf1c76066 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -29,6 +29,9 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_GOTPCREL, R_X86_64_32, R_X86_64_32S, + R_X86_64_DTPMOD64, + R_X86_64_DTPOFF64, + R_X86_64_TPOFF64, R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX, }; diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index c07cb75358..f1734eeba5 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -87,8 +88,8 @@ std::vector GuestImageReader::ReadModuleNames(const std::vector bytes, const bool windows) const { - const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const try { + const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(Io::Utf8Path(path) + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) { std::ostringstream message; @@ -361,6 +362,9 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; +} catch (Domain::RelinkerException& error) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(path); + throw; } } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index de2662f6ad..d7598f0a26 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -285,9 +285,15 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; + std::vector moduleHostLibraries; + std::set uniqueModuleHosts; + for (const auto& image : images) for (const auto& dependency : image.Dependencies) { + if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency); + } + if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx"); for (const auto index : order) { auto& image = images[index]; progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); @@ -332,7 +338,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), hostLibraries.begin(), hostLibraries.end()); + needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end()); output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 65441ef8cb..32cb6523c4 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -84,7 +84,9 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); - return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC); + const bool tls = ref.RelocationTypeValue == R_X86_64_DTPMOD64 || ref.RelocationTypeValue == R_X86_64_DTPOFF64 + || ref.RelocationTypeValue == R_X86_64_TPOFF64; + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | (tls ? STT_TLS : STT_FUNC)); }; static constexpr std::size_t kRelaEntSize = 24; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index a214a56f8d..81c62ebd8e 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -130,6 +130,9 @@ std::vector ElfReader::ReadProgramHeaders() const { ph.MemorySize = _readU64At(offset + 0x28); ph.Alignment = _readU64At(offset + 0x30); + if (ph.Type == PT_LOAD && ph.FileSize > ph.MemorySize) + throw RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); + headers.push_back(ph); offset += header.ProgramHeaderEntrySize; } diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 92c5cee93b..de255c214d 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -35,7 +35,7 @@ namespace Relinker { Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {} -ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const { +ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const try { Domain::InputProgress inputProgress(progress, Io::Utf8Path(input.InputPath)); static_assert(std::endian::native == std::endian::little); if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system"); @@ -46,6 +46,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (input.InputPath.lexically_normal() == input.OutputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite the input executable"); if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); if (options.WindowsDiagnostics && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Windows diagnostics require the Windows target"); + if (options.LinuxFixedBase && options.Target != TargetSystem::Linux) throw Domain::RelinkerException("A fixed Linux base requires the Linux target"); const bool windows = options.Target == TargetSystem::Windows; const bool macos = options.Target == TargetSystem::MacOs; inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 0, 1}); @@ -128,7 +129,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti byteWriter ), std::make_shared(byteWriter), - byteWriter + byteWriter, + options.LinuxFixedBase ); } @@ -137,7 +139,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti try { executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); } catch (Domain::RelinkerException& error) { - error.InputPath = Io::Utf8Path(input.InputPath); + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); throw; } @@ -146,6 +148,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics)}; +} catch (Domain::RelinkerException& error) { + if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath); + throw; } } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index bd98088047..79bef2ca9e 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -3,19 +3,26 @@ #include #include #include +#include #include -#include #include #include #include #include +#include #include namespace Relinker { using namespace Elfpatcher; +namespace { + +constexpr std::uint16_t kEtRel = 1; + +} + RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel, Domain::IProgress& progress) : elfReader(std::move(elfReader)) , syscallScanner(std::move(syscallScanner)) @@ -36,6 +43,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { case 6: return "R_X86_64_GLOB_DAT"; case 7: return "R_X86_64_JUMP_SLOT"; case 10: return "R_X86_64_32"; + case 16: return "R_X86_64_DTPMOD64"; + case 17: return "R_X86_64_DTPOFF64"; + case 18: return "R_X86_64_TPOFF64"; default: { std::ostringstream oss; oss << "UNKNOWN(" << type << ")"; @@ -45,6 +55,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { } RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf) { + if (elfReader->ReadHeader().Type == kEtRel) + throw RelinkerException("ET_REL relocatable inputs are not supported"); + auto programHeaders = elfReader->ReadProgramHeaders(); std::vector textSection; @@ -103,7 +116,35 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (const auto& t : dynTags) if (t.Tag == tag) return t.Value; - throw RelinkerException("DT tag not found"); + const char* name = "unknown"; + switch (tag) { + case DT_OS_STRTAB: name = "DT_OS_STRTAB"; break; + case DT_STRTAB: name = "DT_STRTAB"; break; + case DT_OS_STRSZ: name = "DT_OS_STRSZ"; break; + case DT_STRSZ: name = "DT_STRSZ"; break; + case DT_OS_SYMTAB: name = "DT_OS_SYMTAB"; break; + case DT_SYMTAB: name = "DT_SYMTAB"; break; + case DT_OS_SYMENT: name = "DT_OS_SYMENT"; break; + case DT_SYMENT: name = "DT_SYMENT"; break; + case DT_OS_PLTGOT: name = "DT_OS_PLTGOT"; break; + case DT_PLTGOT: name = "DT_PLTGOT"; break; + case DT_OS_PLTREL: name = "DT_OS_PLTREL"; break; + case DT_PLTREL: name = "DT_PLTREL"; break; + case DT_OS_PLTRELSZ: name = "DT_OS_PLTRELSZ"; break; + case DT_PLTRELSZ: name = "DT_PLTRELSZ"; break; + case DT_OS_JMPREL: name = "DT_OS_JMPREL"; break; + case DT_JMPREL: name = "DT_JMPREL"; break; + case DT_OS_RELA: name = "DT_OS_RELA"; break; + case DT_RELA: name = "DT_RELA"; break; + case DT_OS_RELASZ: name = "DT_OS_RELASZ"; break; + case DT_RELASZ: name = "DT_RELASZ"; break; + case DT_OS_RELAENT: name = "DT_OS_RELAENT"; break; + case DT_RELAENT: name = "DT_RELAENT"; break; + case DT_OS_SYMTABSZ: name = "DT_OS_SYMTABSZ"; break; + } + std::ostringstream message; + message << "Required dynamic tag " << name << " (0x" << std::hex << tag << ") not found"; + throw RelinkerException(message.str()); }; auto requireExactlyOneOf = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) { @@ -111,8 +152,11 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const bool hasSysv = hasTag(sysvTag); if (hasOs && hasSysv) throw RelinkerException(std::string("Both DT_OS_ and DT_ variants present for ") + name); - if (!hasOs && !hasSysv) - throw RelinkerException(std::string("Neither DT_OS_ nor DT_ variant present for ") + name); + if (!hasOs && !hasSysv) { + std::ostringstream message; + message << "Neither DT_OS_ nor DT_ variant present for " << name << " (tags 0x" << std::hex << osTag << " and 0x" << sysvTag << ")"; + throw RelinkerException(message.str()); + } return hasOs; }; @@ -145,8 +189,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const FileByteOffset dynSymTabOffset = readAsOffset(DT_OS_SYMTAB, DT_SYMTAB, "DT_SYMTAB"); constexpr std::size_t symEntSize = 24; - if (readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT") != symEntSize) - throw RelinkerException("Unsupported DT_SYMENT value"); + const auto symbolEntrySize = readAsSize(DT_OS_SYMENT, DT_SYMENT, "DT_SYMENT"); + if (symbolEntrySize != symEntSize) + throw RelinkerException("Unsupported DT_SYMENT value: " + std::to_string(symbolEntrySize) + "; expected 24 bytes"); FileByteOffset dynJmpRelOffset = 0; if (hasPltRelocations) { @@ -155,20 +200,21 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf ? getTagValue(DT_OS_PLTREL) : getTagValue(DT_PLTREL); if (jmprelType != DT_RELA) - throw RelinkerException("Unsupported DT_PLTREL type"); + throw RelinkerException("Unsupported DT_PLTREL type: " + std::to_string(jmprelType) + "; expected DT_RELA (7)"); dynJmpRelOffset = readAsOffset(DT_OS_JMPREL, DT_JMPREL, "DT_JMPREL"); if (gotSize % 24 != 0) - throw RelinkerException("Invalid DT_PLTRELSZ value"); + throw RelinkerException("Invalid DT_PLTRELSZ value: " + std::to_string(gotSize) + " bytes; expected a multiple of 24 bytes"); } const ByteCount dynJmpRelSize = gotSize; const FileByteOffset dynRelaOffset = readAsOffset(DT_OS_RELA, DT_RELA, "DT_RELA"); const ByteCount dynRelaSize = readAsSize(DT_OS_RELASZ, DT_RELASZ, "DT_RELASZ"); constexpr std::size_t relaEntSize = 24; - if (readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT") != relaEntSize) - throw RelinkerException("Unsupported DT_RELAENT value"); + const auto relocationEntrySize = readAsSize(DT_OS_RELAENT, DT_RELAENT, "DT_RELAENT"); + if (relocationEntrySize != relaEntSize) + throw RelinkerException("Unsupported DT_RELAENT value: " + std::to_string(relocationEntrySize) + "; expected 24 bytes"); if (dynRelaSize % relaEntSize != 0) - throw RelinkerException("Invalid DT_RELASZ value"); + throw RelinkerException("Invalid DT_RELASZ value: " + std::to_string(dynRelaSize) + " bytes; expected a multiple of 24 bytes"); std::vector> neededLibraryNamesByStrOffset; for (const auto& tag : dynTags) @@ -180,6 +226,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf auto policy = std::dynamic_pointer_cast(validationPolicy); const std::vector& raw = elfReader->GetRawBytes(); + const std::optional dynSymTabSize = hasTag(DT_OS_SYMTABSZ) + ? std::optional(getTagValue(DT_OS_SYMTABSZ)) : std::nullopt; if (dynStrTabOffset > raw.size() || dynStrTabSize > raw.size() - dynStrTabOffset) throw RelinkerException("Dynamic string table is out of bounds", dynStrTabOffset); @@ -221,15 +269,45 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; - auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + auto symbolEntryPos = [&](const std::uint32_t symIdx) -> FileByteOffset { + const ByteCount off = static_cast(symIdx) * symEntSize; + const FileByteOffset symOff = dynSymTabOffset + off; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize + || (dynSymTabSize && (off > *dynSymTabSize || symEntSize > *dynSymTabSize - off))) throw RelinkerException("Symbol table entry out of bounds", symOff); + return symOff; + }; + + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint64_t value = 0; std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); return (raw[symOff + 4] & 0xf) == 3 && value == 0; }; + const auto isTlsRelocation = [](const std::uint32_t type) { + return type == R_X86_64_DTPMOD64 || type == R_X86_64_DTPOFF64 || type == R_X86_64_TPOFF64; + }; + + const bool hasTlsSegment = std::any_of(programHeaders.begin(), programHeaders.end(), + [](const ProgramHeader& ph) { return ph.Type == PT_TLS; }); + + const auto definedTlsSymbolValue = [&](const std::uint32_t symIdx, const FileByteOffset pos) -> std::optional { + const FileByteOffset symOff = symbolEntryPos(symIdx); + std::uint16_t section = 0; + std::memcpy(§ion, raw.data() + symOff + 6, sizeof(section)); + if (section == SHN_UNDEF) + return std::nullopt; + if ((raw[symOff + 4] & 15) != STT_TLS) + throw RelinkerException("TLS relocation names a defined symbol that is not STT_TLS", pos); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return value; + }; + const auto definedTlsSymbol = [&](const std::uint32_t symIdx, const FileByteOffset pos) { + return definedTlsSymbolValue(symIdx, pos).has_value(); + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -252,9 +330,10 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; } - const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) - throw RelinkerException("Symbol table entry out of bounds", symOff); + if (isTlsRelocation(relType) && (symIdx == 0 || definedTlsSymbol(symIdx, pos))) + continue; + + const FileByteOffset symOff = symbolEntryPos(symIdx); std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); @@ -280,37 +359,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { static_cast(segmentFileOffset); - VirtualAddress codeEnd = 0; - for (const auto& function : functions) - if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) - codeEnd = std::max(codeEnd, function.End); - if (codeEnd == 0) - continue; - if (codeEnd - segmentVAddr > segment.size()) - throw RelinkerException("Unwind function exceeds executable segment", codeEnd); - if (hasPltRelocations && dynJmpRelSize != 0) { - const auto target = [&](const std::size_t at) { - std::int32_t displacement = 0; - std::memcpy(&displacement, segment.data() + at + 2, sizeof(displacement)); - return segmentVAddr + at + 6 + static_cast(static_cast(displacement)); - }; - const std::size_t lastFunctionEnd = codeEnd - segmentVAddr; - std::optional plt; - for (std::size_t at = lastFunctionEnd; segment.size() >= 16 && at <= segment.size() - 16; ++at) { - if (segment[at] == 0xff && segment[at + 1] == 0x35 && segment[at + 6] == 0xff && segment[at + 7] == 0x25 - && target(at) == gotVAddr + 8 && target(at + 6) == gotVAddr + 16) { - plt = at; - break; - } - } - if (!plt) - throw RelinkerException("PLT not found after the last unwind function", codeEnd); - const ByteCount pltSize = 16 * (1 + dynJmpRelSize / relaEntSize); - if (pltSize > segment.size() - *plt) - throw RelinkerException("PLT exceeds executable segment", segmentVAddr + *plt); - codeEnd = segmentVAddr + *plt + pltSize; - } - segment.resize(codeEnd - segmentVAddr); + if (const auto codeEnd = FindSceCodeEnd(segment, segmentVAddr, functions, gotVAddr, dynJmpRelSize)) + segment.resize(*codeEnd - segmentVAddr); } } @@ -327,6 +377,31 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf validationPolicy->ValidateSyscallAbsence(); + std::vector> exportedSymbols; + std::vector exportedFunctions; + if (hasTag(DT_OS_SYMTABSZ)) { + const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); + if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) + throw RelinkerException("Invalid executable dynamic symbol table size"); + for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + std::uint16_t section = 0; + std::memcpy(§ion, symbol + 6, sizeof(section)); + if (section == 0 || (symbol[4] >> 4) == 0 || (symbol[5] & 3) == 1 || (symbol[5] & 3) == 2) continue; + std::uint32_t sourceName = 0; + std::memcpy(&sourceName, symbol, sizeof(sourceName)); + auto name = readCStr(sourceName); + name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); + exportedSymbols.emplace_back(off, std::move(name)); + const auto binding = symbol[4] >> 4; + if ((binding == STB_GLOBAL || binding == STB_WEAK) && (symbol[4] & 15) == STT_FUNC) { + VirtualAddress address = 0; + std::memcpy(&address, symbol + 8, sizeof(address)); + exportedFunctions.push_back(address); + } + } + } + progress.Report({Domain::ProgressStage::Imports, {}, 0, 1}); const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; @@ -354,7 +429,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf { std::ostringstream message; message << "CFG/GOT filtering skipped: multiple executable segments are not modeled; " << nonPltCount << " -> " << nonPltCount << "; filtered=0\n"; progress.Message(message.str()); } } else { - nonPltRefs = unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr); + nonPltRefs = unusedNidFilter->Filter(nonPltRefs, raw, textSection, textVAddr, exportedFunctions); if (nonPltRefs.size() > nonPltCount) throw RelinkerException("Unused NID filter increased the reference count"); { std::ostringstream message; message << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; progress.Message(message.str()); } } @@ -385,26 +460,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (auto declared = Domain::DeclaredImportModule(ref.Nid, importModules); !declared.empty()) dynSection.UnmatchedImportModules.insert(std::move(declared)); } - if (hasTag(DT_OS_SYMTABSZ)) { - const auto symbolBytes = getTagValue(DT_OS_SYMTABSZ); - if (symbolBytes % symEntSize != 0 || dynSymTabOffset > raw.size() || symbolBytes > raw.size() - dynSymTabOffset) - throw RelinkerException("Invalid executable dynamic symbol table size"); - for (ByteCount off = symEntSize; off < symbolBytes; off += symEntSize) { - const auto* symbol = raw.data() + dynSymTabOffset + off; - std::uint16_t section = 0; - std::memcpy(§ion, symbol + 6, sizeof(section)); - if (section == 0 || (symbol[4] >> 4) == 0 || (symbol[5] & 3) == 1 || (symbol[5] & 3) == 2) continue; - std::uint32_t sourceName = 0; - std::memcpy(&sourceName, symbol, sizeof(sourceName)); - auto name = readCStr(sourceName); - name.resize(name.find('#') == std::string::npos ? name.size() : name.find('#')); - const auto nameOffset = static_cast(dynSection.DynStrData.size()); - dynSection.DynStrData.insert(dynSection.DynStrData.end(), name.begin(), name.end()); - dynSection.DynStrData.push_back(0); - const auto position = dynSection.DynSymData.size(); - dynSection.DynSymData.insert(dynSection.DynSymData.end(), symbol, symbol + symEntSize); - std::memcpy(dynSection.DynSymData.data() + position, &nameOffset, sizeof(nameOffset)); - } + for (const auto& [off, name] : exportedSymbols) { + const auto* symbol = raw.data() + dynSymTabOffset + off; + const auto nameOffset = static_cast(dynSection.DynStrData.size()); + dynSection.DynStrData.insert(dynSection.DynStrData.end(), name.begin(), name.end()); + dynSection.DynStrData.push_back(0); + const auto position = dynSection.DynSymData.size(); + dynSection.DynSymData.insert(dynSection.DynSymData.end(), symbol, symbol + symEntSize); + std::memcpy(dynSection.DynSymData.data() + position, &nameOffset, sizeof(nameOffset)); } auto appendRela = [&](std::vector& buf, std::uint64_t offset, std::uint64_t info, std::int64_t addend) { @@ -415,7 +478,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(buf.data() + pos + 16, &addend, 8); }; - auto extractRelative = [&](const FileByteOffset relaOff, const ByteCount relaSize) { + auto extractLocal = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); std::uint64_t rOffset = 0, rInfo = 0; @@ -423,14 +486,32 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); + const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) { appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); + continue; + } + if (!isTlsRelocation(relType)) + continue; + std::uint64_t offset = 0; + if (symIdx != 0) { + const auto value = definedTlsSymbolValue(symIdx, pos); + if (!value) + continue; + offset = *value; + } + if (!hasTlsSegment) + throw RelinkerException("TLS relocation in an executable without a PT_TLS segment", pos); + if (relType == R_X86_64_DTPMOD64 && rAddend != 0) + throw RelinkerException("DTPMOD64 relocation has a nonzero addend", pos); + const auto addend = relType == R_X86_64_DTPMOD64 ? 0 : static_cast(offset + static_cast(rAddend)); + appendRela(dynSection.RelaData, rOffset, static_cast(relType), addend); } }; - extractRelative(dynRelaOffset, dynRelaSize); - extractRelative(dynJmpRelOffset, dynJmpRelSize); + extractLocal(dynRelaOffset, dynRelaSize); + extractLocal(dynJmpRelOffset, dynJmpRelSize); std::vector entries; entries.reserve(nidRefs.size()); diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp index fceff1c265..c286f1c224 100644 --- a/core/relinker/relinker/tests/CallSiteIndexTests.cpp +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -32,6 +32,13 @@ int main() { try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } catch (const std::exception&) { rejected = true; } Require(rejected); - std::cout << "10 call-site index cases passed\n"; + rejected = false; + try { resolver->ResolveCallSites(code, std::numeric_limits::max(), 0x1000, 8); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + constexpr std::uint64_t highBase = 0x8000000000000000ULL; + std::vector highCode{0xFF, 0x15, 0xFA, 0xFF, 0xFF, 0xFF}; + Require(resolver->ResolveCallSites(highCode, highBase + 0x100, highBase + 0x100, 8) == std::vector{highBase + 0x100}); + std::cout << "12 call-site index cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index a2ff847e12..5a5e0b20e3 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -118,6 +119,24 @@ void convert() { require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); } +void fixedBase() { + TestProgress progress; + const Relinker::Conversion converter(progress); + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto plain = converter.Convert(source, {}); + Relinker::ConversionOptions options; + options.LinuxFixedBase = 0x400000000; + const auto fixed = converter.Convert(source, options); + require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC"); + require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB"); + require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module"); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) { + options.Target = target; + reject([&] { converter.Convert(source, options); }); + } +} + void scanning() { TestProgress progress; const auto scanner = Relinker::MakeSyscallScanner(progress); @@ -162,6 +181,33 @@ void scanning() { require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); } +class FailingProgress final : public Domain::IProgress { +public: + std::string InputPath; + void Report(const Domain::ProgressEvent&) override { + Domain::RelinkerException error("diagnostic context", 0x1234); + error.InputPath = InputPath; + throw error; + } + void Message(std::string_view) override {} +}; + +void diagnosticContext() { + for (const bool specific : {false, true}) { + FailingProgress progress; + if (specific) progress.InputPath = "virtual/title/sce_module/plugin.prx"; + const auto source = input(); + try { Relinker::Conversion(progress).Convert(source, {}); } + catch (const Domain::RelinkerException& error) { + require(error.InputPath == (specific ? progress.InputPath : Io::Utf8Path(source.InputPath)), "Wrong diagnostic input path"); + require(error.FailureOffset == 0x1234, "Diagnostic offset changed"); + require(std::string(error.what()) == "diagnostic context", "Diagnostic message changed"); + continue; + } + throw std::runtime_error("Diagnostic failure was swallowed"); + } +} + class CancellingProgress final : public Domain::IProgress { public: Domain::ProgressStage Stage = Domain::ProgressStage::Decode; @@ -191,7 +237,9 @@ void cancellation() { int main() { try { convert(); + fixedBase(); scanning(); + diagnosticContext(); cancellation(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index bc31840e61..4da45f6d6e 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -271,6 +271,45 @@ void exceptionLandingPads() { emit(input, 31, {0xC3}); importThunk(input, 64, 0x2000); require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Exception-only import was removed"); + const auto checkLanding = [&](std::uint64_t base, std::uint64_t offset, bool signedOffset, bool rejected) { + auto data = bytes; + write(data, 0x280, 0); + write(data, 0x281, base); + write(data, 0x289, 0xFF); + write(data, 0x28A, signedOffset ? 12 : 4); + write(data, 0x28B, 25); + write(data, 0x28C, 0); + write(data, 0x294, 1); + write(data, 0x29C, offset); + write(data, 0x2A4, 0); + try { + const auto functions = Relinker::UnusedNidFilter::ReadExceptionFunctions(data, headers, {}, {}); + require(!rejected, "Overflowing LSDA landing pad address was accepted"); + const auto expected = offset == 0 ? std::vector{0x1000} : + std::vector{0x1000, base + offset}; + require(functions.size() == 1 && functions[0].ExtraTargets == expected, + "Valid LSDA landing pad address changed"); + } catch (const Relinker::RelinkerException& error) { + require(rejected && std::string(error.what()).find("LSDA landing pad address overflowed") != std::string::npos, + "LSDA landing pad rejected for an unexpected reason"); + } + }; + const auto maximum = std::numeric_limits::max(); + for (const std::uint64_t base : {std::uint64_t{0x1000}, maximum - 16}) { + checkLanding(base, 0, false, false); + checkLanding(base, maximum - base, false, false); + checkLanding(base, maximum - base + 1, false, true); + checkLanding(base, maximum, false, true); + } + checkLanding(0x1000, maximum, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1000, true, false); + checkLanding(0x1000, std::uint64_t{0} - 0x1001, true, true); + checkLanding(maximum - 16, 16, true, false); + checkLanding(maximum - 16, 17, true, true); + checkLanding(0, 0, true, false); + checkLanding(0, maximum, false, false); + checkLanding(std::uint64_t{1} << 63, std::uint64_t{1} << 63, true, false); + checkLanding((std::uint64_t{1} << 63) - 1, std::uint64_t{1} << 63, true, true); write(bytes, 0x282, 0xFF); requireFailure([&] { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); }, "Malformed LSDA was accepted"); } @@ -398,7 +437,9 @@ void relativeRelocationTableBounds() { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); diff --git a/core/relinker/relinker/tests/guest_import_fixtures.py b/core/relinker/relinker/tests/guest_import_fixtures.py new file mode 100644 index 0000000000..ac961cecf7 --- /dev/null +++ b/core/relinker/relinker/tests/guest_import_fixtures.py @@ -0,0 +1,35 @@ +import struct + +from test_guest_intel_trampolines import main_fixture + + +def dynamic(image, offset, phoff, tags): + for index, tag in enumerate(tags + [(0, 0)]): + struct.pack_into('> 32) * 24) + end = elf.index(b"\0", strings + name) + result.append((target, info & 0xffffffff, elf[strings + name:end].decode(), symbol_info & 15, addend)) + return result + + +def macho_bytes_at(data, address, size): + for segment in MachO(data).segments: + if segment["vmaddr"] <= address and address + size <= segment["vmaddr"] + segment["filesize"]: + offset = segment["fileoff"] + address - segment["vmaddr"] + return data[offset:offset + size] + raise AssertionError(f"unmapped address {address:#x}") + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-exec-tls-") as directory: + local = [(0x1000, TPOFF64, NULL, 4), (0x1008, DTPOFF64, NULL, 8), (0x1010, TPOFF64, TLS_SYMBOL, 2)] + result, output = relink(relinker, directory, "linux", image(local + [(0x1018, DTPMOD64, NULL, 0), + (0x1020, TPOFF64, IMPORTED, 0)])) + assert result.returncode == 0, (result.stdout, result.stderr) + elf = output.read_bytes() + relocations = dynamic_relocations(elf) + tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64)) + assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12), + (0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations + assert any(struct.unpack_from("= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - configured_paths = configured.split(":") if label.startswith("list") else [configured] - actual_paths = actual.split(":") if label.startswith("list") else [actual] + path_list = label.startswith("list") or label == "absolute-list" + configured_paths = configured.split(":") if path_list else [configured] + actual_paths = actual.split(":") if path_list else [actual] expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] - if resolved != expected or (label == "absolute" and actual != run_path): + if resolved != expected or (label.startswith("absolute") and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 + for index, run_path in enumerate(("relative/libs", "C:/host/libs", "C:\\host\\libs", "\\\\server\\share")): + output = work / f"invalid-{index}.elf" + result = subprocess.run([str(relinker), "--rpath", run_path, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "run path must be absolute or begin with $ORIGIN" in result.stderr, ( + run_path, result.returncode, result.stdout, result.stderr) + assert not output.exists() assert not failures, failures print(f"Guest run path tests passed: {checked} module/path combinations") diff --git a/core/relinker/relinker/tests/test_guest_tls.py b/core/relinker/relinker/tests/test_guest_tls.py index 4735a3f0b5..6521a2e065 100644 --- a/core/relinker/relinker/tests/test_guest_tls.py +++ b/core/relinker/relinker/tests/test_guest_tls.py @@ -8,7 +8,9 @@ import subprocess import sys import tempfile -from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture, pe_sections +from test_guest_module_directories import module_with_symbol +from guest_import_fixtures import dynamic, executable def guest_with_tls(tls): @@ -40,6 +42,52 @@ def loaded_tls_index(path): return base, ctypes.c_uint32.from_address(index_address).value +def static_tls_getter(alignment, memory_size): + image = module_with_symbol(True) + struct.pack_into(" {kept};" in result.stdout, (name, result.stdout) + if level == 1: + assert f"CFG/GOT filtering: 1 -> {kept};" in result.stdout, (name, result.stdout) return kept == 1 +def exported_fixture(binding, defined, sections): + image = build(at_offset([(0, b"\xc3"), (0x40, load_slot(FUNCTION))])) + strings = b"\0foo\0lib.so\0exported\0" + image[0x600:0x600 + len(strings)] = strings + struct.pack_into(" 0, "executable fixture has no base relocations" + assert dll_characteristics(executable) == NX_COMPAT, f"DllCharacteristics {dll_characteristics(executable):#x}" + assert image_base(executable) < GUEST_ARENA_START, f"ImageBase {image_base(executable):#x}" if os.name == "nt": for _ in range(8): executed = subprocess.run([str(output)], capture_output=True, text=True, timeout=20) assert executed.returncode < GUEST_ARENA_START >> 32, \ f"stack not below {GUEST_ARENA_START:#x}: exit code {executed.returncode:#x}" + module_case = work / "module" + (module_case / "prx").mkdir(parents=True) + (module_case / "prx" / "provider.prx").write_bytes(module_with_relative_relocation()) + module_source = module_case / "input.elf" + module_source.write_bytes(fixture()) + module_output = module_case / "output.exe" + result = subprocess.run([str(relinker), "--windows", str(module_source), str(module_output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = module_case / "app0" / "prx" / "provider.prx.guest.prx" + assert artifact.is_file(), list((module_case / "app0").rglob("*")) + module = artifact.read_bytes() + assert relocation_directory_size(module) > 0, "module fixture has no base relocations" + assert dll_characteristics(module) == DYNAMIC_BASE | NX_COMPAT, \ + f"DllCharacteristics {dll_characteristics(module):#x}" print("Windows address space integration tests passed") diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py index cd5d8cd5f2..85a0429cea 100644 --- a/core/relinker/relinker/tests/test_windows_fixed_base.py +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -70,7 +70,7 @@ def main(): assert bool(struct.unpack_from("inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); + append(key, tessellation->primitiveGroupSize); + append(key, tessellation->breakAtInstance); } template @@ -180,6 +182,7 @@ private: append(key, value.orderedPixelShader); append(key, value.alphaToCoverage); append(key, value.stencilRefExportEnable); + append(key, value.ditheredAlphaToCoverage); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index ada6f1084e..0b7cb0fb7d 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -10,6 +10,14 @@ namespace ShaderRecompiler { +struct UserDataTail { + std::uint32_t programCounter; + std::uint32_t userDataRegister; +}; + +[[nodiscard]] std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount); +[[nodiscard]] std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation); + struct SwappcInfo { bool fetchCallAllowed = false; std::uint32_t userDataBaseRegister = 0; diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp index 20cd18e3ad..ac92180cb2 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/Structurizer.hpp @@ -11,6 +11,7 @@ public: private: void structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const; + void structurizeWithFlags(ControlFlowGraph& graph) const; void structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const; void privatizeSharedReturns(ControlFlowGraph& graph) const; void computeDominatorTree(ControlFlowGraph& graph) const; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 3b623565eb..48bdd33cd0 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1011,6 +1011,34 @@ std::vector FindUserDataCalls(const RdnaProgram& program, const Sw return calls; } +std::optional FindUserDataTail(const RdnaProgram& program, std::uint32_t userDataBase, std::uint32_t userDataCount) { + if (program.instructions.size() < 2u || program.instructions.back().op != RdnaOpcode::SCodeEnd) return std::nullopt; + const auto& tail = program.instructions[program.instructions.size() - 2u]; + if (tail.op != RdnaOpcode::SSetpcB64 || tail.source0.kind != RdnaOperandKind::ScalarRegister) return std::nullopt; + const auto reg = tail.source0.reg; + if (reg % 2u != 0u || reg < userDataBase || userDataCount < 2u || reg - userDataBase >= userDataCount - 1u) return std::nullopt; + for (const auto& instruction : program.instructions) { + if (instruction.programCounter >= tail.programCounter) break; + if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SCallB64 || instruction.op == RdnaOpcode::SSwappcB64 || + instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm || instruction.op == RdnaOpcode::SCodeEnd || + writesScalar(instruction, reg) || writesScalar(instruction, reg + 1u)) return std::nullopt; + } + return UserDataTail{tail.programCounter, reg}; +} + +std::vector LinkUserDataTail(const RdnaProgram& program, const UserDataTail& tail, const RdnaProgram& continuation) { + const auto recognized = FindUserDataTail(program, tail.userDataRegister, 2u); + if (!recognized || recognized->programCounter != tail.programCounter) throw std::invalid_argument("user-data tail is not a linear jump through an unchanged register pair"); + for (const auto& instruction : continuation.instructions) { + if (instruction.op == RdnaOpcode::SGetpcB64) throw std::invalid_argument("user-data tail continuation reads its program counter, which a linked copy does not keep"); + } + const auto words = tail.programCounter / 4u; + std::vector linked(program.code.begin(), program.code.begin() + words); + linked.push_back(0xbf820000u); + linked.insert(linked.end(), continuation.code.begin(), continuation.code.end()); + return linked; +} + std::optional UserDataCalleeLength(std::span callee, std::uint32_t linkRegister) { std::uint32_t furthest = 0; for (std::uint32_t word = 0; word < callee.size();) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 217d54eee7..93b1024051 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -153,6 +153,8 @@ public: bool ReadBool() { return ReadU8() != 0u; } + std::uint64_t Remaining() const { return m_data.size() - m_offset; } + std::uint32_t ReadU32() { std::uint32_t value = 0u; for (std::uint32_t i = 0; i < 4u; i++) { @@ -336,6 +338,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.stencilRefExportEnable); writer.WriteU8(static_cast(info.rasterPrimitive)); writer.WriteBool(info.layerExported); + writer.WriteBool(info.ditheredAlphaToCoverage); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -387,6 +390,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); if (version >= 23u) info.layerExported = reader.ReadBool(); + if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); 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) | @@ -523,6 +527,8 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.partitioning); writer.WriteU32(configuration.outputTopology); writer.WriteU32(configuration.patchesPerGroup); + writer.WriteU32(configuration.primitiveGroupSize); + writer.WriteBool(configuration.breakAtInstance); } TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { @@ -533,6 +539,10 @@ TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uin configuration.partitioning = reader.ReadU32(); configuration.outputTopology = reader.ReadU32(); if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32(); + if (version >= 24u) { + configuration.primitiveGroupSize = reader.ReadU32(); + configuration.breakAtInstance = reader.ReadBool(); + } return configuration; } @@ -752,7 +762,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(23u); + writer.WriteU32(25u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -784,7 +794,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 > 23u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 25u) 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); @@ -808,7 +818,16 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const if (version >= 17u) result.request.target.registrationModules = reader.ReadBool(); if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32(); if (version >= 18u) result.request.target.vertexSubgroups = reader.ReadBool(); - if (version >= 21u) result.request.target.float16DenormPreserve = reader.ReadBool(); + if (version >= 21u) { + if (version >= 22u) reader.Require(1u); + if (reader.Remaining() == 1u) { + const auto float16DenormPreserve = reader.ReadU8(); + if (float16DenormPreserve > 1u) throw std::runtime_error("RequestSerializer: invalid float16DenormPreserve value"); + result.request.target.float16DenormPreserve = float16DenormPreserve != 0u; + } else if (reader.Remaining() != 0u) { + throw std::runtime_error("RequestSerializer: unexpected trailing data"); + } + } return result; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index f699c1e3df..c78d429445 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -22,6 +22,29 @@ std::vector allBlockIds(std::uint32_t count) { return ids; } +std::vector reversePostOrder(const ControlFlowGraph& graph); + +std::vector analysisOrder(const ControlFlowGraph& graph, bool successorsFirst) { + const auto count = static_cast(graph.blocks.size()); + std::vector order; + std::vector placed(count, false); + if (graph.entryBlock < count) { + order = reversePostOrder(graph); + if (successorsFirst) { + std::reverse(order.begin(), order.end()); + } + for (const auto id : order) { + placed[id] = true; + } + } + for (std::uint32_t id = 0; id < count; ++id) { + if (!placed[id]) { + order.push_back(id); + } + } + return order; +} + bool inPlaceIntersections() { static const bool enabled = std::getenv("APS5_NO_PERF_DOM_INTERSECTION") == nullptr; return enabled; @@ -316,43 +339,47 @@ bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& lo } std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t trueTarget, std::uint32_t falseTarget, std::uint32_t globalMerge) { - std::vector reached; + std::vector reached(graph.blocks.size(), false); std::vector pending = {trueTarget, falseTarget}; while (!pending.empty()) { const auto blockId = pending.back(); pending.pop_back(); - if (blockId == globalMerge || contains(reached, blockId) || !isInsideLoopConstruct(graph, loop, blockId)) { + if (blockId == globalMerge || reached[blockId] || !isInsideLoopConstruct(graph, loop, blockId)) { continue; } - reached.push_back(blockId); + reached[blockId] = true; const auto& successors = graph.FindBlock(blockId).successors; pending.insert(pending.end(), successors.begin(), successors.end()); } + std::vector joins(graph.blocks.size(), false); std::vector joining = {globalMerge}; + joins[globalMerge] = true; for (std::size_t index = 0; index < joining.size(); ++index) { for (const auto predecessor : graph.FindBlock(joining[index]).predecessors) { - if (contains(reached, predecessor) && !contains(joining, predecessor)) { + if (reached[predecessor] && !joins[predecessor]) { + joins[predecessor] = true; joining.push_back(predecessor); } } } sortUnique(joining); - if (!contains(joining, trueTarget) || !contains(joining, falseTarget)) { + if (!joins[trueTarget] || !joins[falseTarget]) { return globalMerge; } std::vector> postDominators(graph.blocks.size()); for (const auto blockId : joining) { postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; } + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto blockId : joining) { - if (blockId == globalMerge) { + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) { continue; } auto next = joining; for (const auto successor : graph.FindBlock(blockId).successors) { - if (contains(joining, successor)) { + if (joins[successor]) { next = intersectSorted(next, postDominators[successor]); } } @@ -433,9 +460,11 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock postDominators[block.id] = {block.id}; } } + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto& block : graph.blocks) { + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; if (kept[block.id].empty()) { continue; } @@ -443,8 +472,7 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock for (std::size_t i = 1; i < kept[block.id].size(); ++i) { intersectAnalysisSet(next, postDominators[kept[block.id][i]]); } - addUnique(next, block.id); - sortUnique(next); + addSortedUnique(next, block.id); if (next != postDominators[block.id]) { replaceAnalysisSet(postDominators[block.id], std::move(next)); changed = true; @@ -506,9 +534,11 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur } std::vector> postDominators(count, allBlockIds(count)); postDominators[join] = {join}; + const auto order = analysisOrder(graph, true); for (bool changed = true; changed;) { changed = false; - for (const auto& block : graph.blocks) { + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; if (!joins[block.id] || block.id == join) { continue; } @@ -810,6 +840,8 @@ constexpr std::uint32_t CloneWordLimit = 1024u; constexpr std::uint32_t CloneWordFloor = 256u; constexpr std::uint32_t CloneShareDivisor = 8u; constexpr std::uint32_t CloneBudgetDivisor = 4u; +constexpr std::uint32_t SplitGrowthFactor = 16u; +constexpr std::uint32_t SplitGrowthFloor = 64u; struct SplitBudget { std::uint32_t programWords = 0; @@ -1450,10 +1482,328 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { state.components.push_back(std::move(component)); } +bool forcedFlagStructurization() { + static const bool forced = std::getenv("APS5_FORCE_STRUCTURIZER_FALLBACK") != nullptr; + return forced; +} + +std::vector terminatorTargets(const Terminator& terminator) { + if (terminator.kind == TerminatorKind::Branch) { + return {terminator.trueBlock}; + } + if (terminator.kind == TerminatorKind::ConditionalBranch) { + return {terminator.trueBlock, terminator.falseBlock}; + } + return {}; +} + +bool supportsFlagStructurization(const ControlFlowGraph& graph) { + const auto count = graph.blocks.size(); + if (graph.unsupported || graph.entryBlock >= count) { + return false; + } + for (std::uint32_t id = 0; id < count; ++id) { + const auto& block = graph.blocks[id]; + const auto kind = block.terminator.kind; + if (block.id != id || (kind != TerminatorKind::Branch && kind != TerminatorKind::ConditionalBranch && kind != TerminatorKind::Return)) { + return false; + } + const auto targets = terminatorTargets(block.terminator); + if (std::any_of(targets.begin(), targets.end(), [&](std::uint32_t target) { return target >= count; })) { + return false; + } + } + return true; +} + +struct FlagEmission { + ControlFlowGraph* graph = nullptr; + std::uint32_t entryBlock = InvalidControlFlowId; + std::uint32_t firstVariable = 0; + std::uint32_t nextVariable = 0; + std::vector> targets; + std::vector> successors; + std::vector> predecessors; + std::vector> openLoops; + std::vector order; + std::vector fallthroughs; +}; + +std::vector> orderedComponents(const FlagEmission& emission, const std::vector& members) { + const auto count = emission.successors.size(); + std::vector inside(count, false); + for (const auto member : members) { + inside[member] = true; + } + std::vector index(count, InvalidControlFlowId); + std::vector lowlink(count, InvalidControlFlowId); + std::vector componentOf(count, InvalidControlFlowId); + std::vector onStack(count, false); + std::vector stack; + std::vector> work; + std::vector> components; + std::uint32_t nextIndex = 0; + const auto visit = [&](std::uint32_t blockId) { + index[blockId] = nextIndex; + lowlink[blockId] = nextIndex; + ++nextIndex; + stack.push_back(blockId); + onStack[blockId] = true; + work.emplace_back(blockId, 0u); + }; + for (const auto root : members) { + if (index[root] != InvalidControlFlowId) { + continue; + } + visit(root); + while (!work.empty()) { + const auto blockId = work.back().first; + const auto& successors = emission.successors[blockId]; + if (work.back().second < successors.size()) { + const auto successor = successors[work.back().second++]; + if (!inside[successor]) { + continue; + } + if (index[successor] == InvalidControlFlowId) { + visit(successor); + } else if (onStack[successor]) { + lowlink[blockId] = std::min(lowlink[blockId], index[successor]); + } + continue; + } + work.pop_back(); + if (!work.empty()) { + auto& parent = lowlink[work.back().first]; + parent = std::min(parent, lowlink[blockId]); + } + if (lowlink[blockId] != index[blockId]) { + continue; + } + std::vector component; + for (;;) { + const auto member = stack.back(); + stack.pop_back(); + onStack[member] = false; + componentOf[member] = static_cast(components.size()); + component.push_back(member); + if (member == blockId) { + break; + } + } + sortUnique(component); + components.push_back(std::move(component)); + } + } + + std::vector incoming(components.size(), 0u); + std::vector> following(components.size()); + for (const auto member : members) { + for (const auto successor : emission.successors[member]) { + if (inside[successor] && componentOf[successor] != componentOf[member]) { + following[componentOf[member]].push_back(componentOf[successor]); + } + } + } + for (auto& next : following) { + sortUnique(next); + for (const auto component : next) { + ++incoming[component]; + } + } + std::map ready; + for (std::uint32_t component = 0; component < components.size(); ++component) { + if (incoming[component] == 0u) { + ready.emplace(components[component].front(), component); + } + } + std::vector> ordered; + ordered.reserve(components.size()); + while (!ready.empty()) { + const auto component = ready.begin()->second; + ready.erase(ready.begin()); + for (const auto next : following[component]) { + if (--incoming[next] == 0u) { + ready.emplace(components[next].front(), next); + } + } + ordered.push_back(std::move(components[component])); + } + return ordered; +} + +std::uint32_t flagLoopHead(const FlagEmission& emission, const std::vector& component) { + for (const auto member : component) { + const auto& predecessors = emission.predecessors[member]; + if (member == emission.entryBlock || std::any_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return !std::binary_search(component.begin(), component.end(), predecessor); })) { + return member; + } + } + return component.front(); +} + +std::uint32_t appendFlagBlock(FlagEmission& emission, std::uint32_t positionBlock) { + const auto block = appendRouteBlock(*emission.graph, positionBlock); + emission.order.push_back(block); + return block; +} + +std::uint32_t appendFlagAssignment(FlagEmission& emission, std::uint32_t positionBlock, std::uint32_t variable, std::int32_t value) { + const auto assignment = appendAssignmentBlock(*emission.graph, positionBlock, positionBlock, variable, value); + emission.order.push_back(assignment); + return assignment; +} + +void setDispatch(BasicBlock& block, std::uint32_t variable, std::uint32_t trueTarget, std::uint32_t falseTarget) { + block.successors = {trueTarget, falseTarget}; + block.terminator.kind = TerminatorKind::ConditionalBranch; + block.terminator.condition = BranchCondition::GotoVariable; + block.terminator.trueBlock = trueTarget; + block.terminator.falseBlock = falseTarget; + block.terminator.gotoVariable = variable; +} + +std::vector alwaysNextComponents(const FlagEmission& emission, const std::vector>& components, const std::vector& firsts) { + const auto count = static_cast(components.size()); + std::vector componentOf(emission.successors.size(), InvalidControlFlowId); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + componentOf[member] = component; + } + } + std::vector next(count, true); + std::vector skipped(count + 1u, 0); + for (std::uint32_t component = 0; component < count; ++component) { + for (const auto member : components[component]) { + for (const auto successor : emission.successors[member]) { + const auto target = componentOf[successor]; + if (target <= component) { + continue; + } + ++skipped[component + 1u]; + --skipped[target]; + if (successor != firsts[target]) { + next[target] = false; + } + } + } + } + const auto entryComponent = componentOf[emission.entryBlock]; + std::int64_t crossing = 0; + for (std::uint32_t component = 0; component < count; ++component) { + crossing += skipped[component]; + if (crossing > 0 || component < entryComponent || (component == entryComponent && firsts[component] != emission.entryBlock)) { + next[component] = false; + } + } + return next; +} + +void emitFlagBlock(FlagEmission& emission, std::uint32_t original, bool guarded) { + auto& graph = *emission.graph; + const auto reach = emission.firstVariable + original; + const auto guard = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto clear = appendFlagAssignment(emission, original, reach, 0); + emission.fallthroughs.push_back(clear); + emission.order.push_back(original); + + std::vector edges; + std::vector ends; + for (const auto target : emission.targets[original]) { + const auto edge = appendFlagAssignment(emission, original, emission.firstVariable + target, 1); + edges.push_back(edge); + const auto loop = std::find_if(emission.openLoops.begin(), emission.openLoops.end(), [&](const std::pair& open) { return open.first == target; }); + if (loop == emission.openLoops.end()) { + ends.push_back(edge); + continue; + } + emission.fallthroughs.push_back(edge); + ends.push_back(appendFlagAssignment(emission, original, loop->second, 1)); + } + + const bool conditional = graph.blocks[original].terminator.kind == TerminatorKind::ConditionalBranch; + const auto join = conditional ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto merge = guarded ? appendFlagBlock(emission, original) : InvalidControlFlowId; + const auto exit = conditional ? join : merge; + for (const auto end : ends) { + if (exit == InvalidControlFlowId) { + emission.fallthroughs.push_back(end); + } else { + setBranch(graph.blocks[end], exit); + } + } + if (conditional) { + emission.fallthroughs.push_back(join); + } + if (guarded) { + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[guard], reach, clear, merge); + graph.blocks[guard].terminator.mergeBlock = merge; + } + + auto& block = graph.blocks[original]; + block.successors = edges; + block.terminator.mergeBlock = join; + block.terminator.continueBlock = InvalidControlFlowId; + block.terminator.loopHeader = false; + if (!edges.empty()) { + block.terminator.trueBlock = edges.front(); + } + if (conditional) { + block.terminator.falseBlock = edges.back(); + } +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members); + +void emitFlagLoop(FlagEmission& emission, std::vector component, std::uint32_t head, bool guarded) { + auto& graph = *emission.graph; + const auto again = emission.nextVariable++; + const auto header = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(header); + emission.fallthroughs.push_back(appendFlagAssignment(emission, head, again, 0)); + emission.openLoops.emplace_back(head, again); + emitFlagBlock(emission, head, guarded); + component.erase(std::find(component.begin(), component.end(), head)); + emitFlagRegion(emission, component); + emission.openLoops.pop_back(); + const auto latch = appendFlagBlock(emission, head); + const auto merge = appendFlagBlock(emission, head); + emission.fallthroughs.push_back(merge); + setDispatch(graph.blocks[latch], again, header, merge); + auto& terminator = graph.blocks[header].terminator; + terminator.loopHeader = true; + terminator.mergeBlock = merge; + terminator.continueBlock = latch; +} + +void emitFlagRegion(FlagEmission& emission, const std::vector& members) { + const auto components = orderedComponents(emission, members); + std::vector loops; + std::vector firsts; + for (const auto& component : components) { + const auto first = component.front(); + loops.push_back(component.size() != 1u || contains(emission.successors[first], first)); + firsts.push_back(loops.back() ? flagLoopHead(emission, component) : first); + } + const auto next = emission.openLoops.empty() ? alwaysNextComponents(emission, components, firsts) : std::vector(components.size(), false); + for (std::size_t index = 0; index < components.size(); ++index) { + if (loops[index]) { + emitFlagLoop(emission, components[index], firsts[index], !next[index]); + } else { + emitFlagBlock(emission, firsts[index], !next[index] || emission.graph->blocks[firsts[index]].terminator.kind == TerminatorKind::Return); + } + } +} + } void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; + const bool flagsSupported = supportsFlagStructurization(original); + if (flagsSupported && forcedFlagStructurization()) { + structurizeWithFlags(graph); + return; + } try { structurizeAttempts(graph, false); } catch (const std::runtime_error&) { @@ -1462,11 +1812,87 @@ void Structurizer::Structurize(ControlFlowGraph& graph) const { try { structurizeAttempts(graph, true); } catch (const std::runtime_error&) { - std::rethrow_exception(error); + if (!flagsSupported) { + std::rethrow_exception(error); + } + graph = original; + try { + structurizeWithFlags(graph); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } } } } +void Structurizer::structurizeWithFlags(ControlFlowGraph& graph) const { + if (!supportsFlagStructurization(graph)) { + throw std::runtime_error("CFG flag structurization supports only branch, conditional branch and return blocks"); + } + const auto count = static_cast(graph.blocks.size()); + const auto entry = graph.entryBlock; + FlagEmission emission; + emission.graph = &graph; + emission.entryBlock = entry; + emission.firstVariable = nextGotoVariable(graph); + emission.nextVariable = emission.firstVariable + count; + emission.targets.resize(count); + emission.successors.resize(count); + emission.predecessors.resize(count); + for (const auto& block : graph.blocks) { + emission.targets[block.id] = terminatorTargets(block.terminator); + emission.successors[block.id] = emission.targets[block.id]; + sortUnique(emission.successors[block.id]); + for (const auto successor : emission.successors[block.id]) { + emission.predecessors[successor].push_back(block.id); + } + } + + emitFlagRegion(emission, allBlockIds(count)); + const auto body = std::move(emission.order); + emission.order.clear(); + for (auto variable = emission.firstVariable; variable < emission.nextVariable; ++variable) { + emission.fallthroughs.push_back(appendFlagAssignment(emission, entry, variable, variable == emission.firstVariable + entry ? 1 : 0)); + } + emission.order.insert(emission.order.end(), body.begin(), body.end()); + appendFlagBlock(emission, entry); + + const auto& order = emission.order; + std::vector placed(graph.blocks.size(), false); + for (const auto blockId : order) { + if (blockId >= placed.size() || placed[blockId]) { + throw std::runtime_error("CFG flag structurization placed a block twice"); + } + placed[blockId] = true; + } + if (order.size() != graph.blocks.size()) { + throw std::runtime_error("CFG flag structurization did not place every block"); + } + std::vector fallthrough(graph.blocks.size(), false); + for (const auto blockId : emission.fallthroughs) { + fallthrough[blockId] = true; + } + for (std::size_t position = 0; position + 1u < order.size(); ++position) { + if (fallthrough[order[position]]) { + setBranch(graph.blocks[order[position]], order[position + 1u]); + } + } + + graph.entryBlock = order.front(); + std::vector blocks; + blocks.reserve(order.size()); + for (const auto blockId : order) { + blocks.push_back(std::move(graph.blocks[blockId])); + } + applyBlockOrder(graph, std::move(blocks)); + rebuildPredecessors(graph); + recomputeAnalyses(graph); + orderByDominance(graph); + if (graph.irreducible || !hasStructuredConstructs(graph)) { + throw std::runtime_error("CFG flag structurization did not produce structured constructs"); + } +} + void Structurizer::structurizeAttempts(ControlFlowGraph& graph, bool relaxReturnJoins) const { const auto original = graph; try { @@ -1580,10 +2006,12 @@ void Structurizer::computeDominatorTree(ControlFlowGraph& graph) const { block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; } + const auto order = analysisOrder(graph, false); bool changed = true; while (changed) { changed = false; - for (auto& block : graph.blocks) { + for (const auto id : order) { + auto& block = graph.blocks[id]; if (block.id == graph.entryBlock) { continue; } @@ -1650,19 +2078,7 @@ void Structurizer::computePostDominators(ControlFlowGraph& graph) const { block.postDominators = block.successors.empty() ? std::vector{block.id} : all; } - std::vector order; - order.reserve(count); - std::vector ordered(count, false); - if (graph.entryBlock < count) { - const auto forward = reversePostOrder(graph); - for (auto it = forward.rbegin(); it != forward.rend(); ++it) { - order.push_back(*it); - ordered[*it] = true; - } - } - for (std::uint32_t id = 0; id < count; ++id) { - if (!ordered[id]) order.push_back(id); - } + const auto order = analysisOrder(graph, true); bool changed = true; while (changed) { @@ -1813,12 +2229,16 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeL void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits, bool relaxReturnJoins) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); + const auto growthBudget = std::max(SplitGrowthFloor, originalBlockCount * SplitGrowthFactor); const auto programWords = estimatedSpirvWords(graph, allBlockIds(originalBlockCount)); SplitBudget budget{programWords, std::max(CloneWordLimit, programWords / CloneBudgetDivisor), std::max(CloneWordLimit, programWords), nextGotoVariable(graph)}; for (std::uint32_t splits = 0; splits < splitBudget; ++splits) { if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits, routeSharedExits) && !splitOneReturnJoin(graph, relaxReturnJoins)) { return; } + if (graph.blocks.size() > growthBudget) { + throw std::runtime_error("CFG shared merge splitting exceeded growth budget: originalBlocks=" + std::to_string(originalBlockCount) + " currentBlocks=" + std::to_string(graph.blocks.size()) + " growthBudget=" + std::to_string(growthBudget)); + } rebuildPredecessors(graph); recomputeAnalyses(graph); verifyReducibility(graph); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index 8ffdb9c68a..8d051b4d14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -27,6 +27,7 @@ public: const IrResourcePlan& Resources() const; IrProgramMetadata& Metadata(); const IrProgramMetadata& Metadata() const; + [[nodiscard]] CompiledShaderInfo CompiledInfo() const&; CompiledShaderInfo TakeCompiledInfo() &&; IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7ad5b937ee..7d0a7a6c98 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -309,16 +309,18 @@ const IrProgramMetadata& IrProgram::Metadata() const { return metadata; } -CompiledShaderInfo IrProgram::TakeCompiledInfo() && { +namespace { + +CompiledShaderInfo compiledInfo(const IrResourcePlan& resourcePlan, std::uint32_t waveSize, ShaderInfo info, IrBindingLayout bindings) { CompiledShaderInfo result; result.stage = resourcePlan.stage; result.shaderHash = resourcePlan.shaderHash; result.waveSize = waveSize; result.userDataBase = resourcePlan.userDataBase; result.userDataCount = resourcePlan.userDataCount; - result.scratchDwords = resourcePlan.info.scratchDwords; - result.info = std::move(resourcePlan.info); - result.bindings = std::move(metadata.bindings); + result.scratchDwords = info.scratchDwords; + result.info = std::move(info); + result.bindings = std::move(bindings); for (const auto& output : result.info.outputs) { if (output.kind == StageOutputKind::Parameter && output.index < 32u) { result.paramExportMask |= 1u << output.index; @@ -327,6 +329,16 @@ CompiledShaderInfo IrProgram::TakeCompiledInfo() && { return result; } +} + +CompiledShaderInfo IrProgram::CompiledInfo() const& { + return compiledInfo(resourcePlan, waveSize, resourcePlan.info, metadata.bindings); +} + +CompiledShaderInfo IrProgram::TakeCompiledInfo() && { + return compiledInfo(resourcePlan, waveSize, std::move(resourcePlan.info), std::move(metadata.bindings)); +} + IrValue& IrProgram::CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags) { IrValue& created = CreateValue(opcode, type); created.SetFlags(flags); diff --git a/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp new file mode 100644 index 0000000000..910c3499db --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/RedundantValueEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_REDUNDANTVALUEELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct RedundantValueEliminationStats { + std::uint32_t replacedValues = 0; +}; + +class RedundantValueEliminator { +public: + [[nodiscard]] RedundantValueEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 909f3cea13..77328b8672 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -10,19 +10,26 @@ namespace ShaderRecompiler { struct PreparedControlFlow; +struct SourceEntry; class ShaderPreparationContext { public: [[nodiscard]] std::shared_ptr AcquireFrontend(const RecompileRequest& request); + [[nodiscard]] std::uint32_t ResourcePrograms() const; private: + friend IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); + friend struct PreparedInterface; std::shared_ptr frontend; + std::shared_ptr interfaceSource; + std::unique_ptr interfaceProgram; + std::uint32_t resourcePrograms = 0; }; [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request); [[nodiscard]] IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation); +[[nodiscard]] std::uint64_t FrontEndBuilds(); [[nodiscard]] std::shared_ptr GetResourcePlan(const RecompileRequest& request); -struct SourceEntry; struct ResourceCapture { std::shared_ptr plan; ResourceSnapshot snapshot; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index f5029b374d..77e52d36de 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -113,6 +113,8 @@ struct ShaderTessellationInputInfo { std::uint32_t partitioning = 0; std::uint32_t outputTopology = 0; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; struct ShaderVertexInputInfo { @@ -175,6 +177,7 @@ struct ShaderPixelInputInfo { ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; bool psOrderedPixelShader = false; bool psAlphaToCoverage = false; + bool psDitheredAlphaToCoverage = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp index 7de56772ea..d448da5e6a 100644 --- a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -51,6 +51,8 @@ private: case IrOpcode::ConvertF32S32: case IrOpcode::ConvertF32U32: case IrOpcode::ConvertF32F16: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: return true; case IrOpcode::BitCastU32F32: case IrOpcode::BitCastF32U32: @@ -67,6 +69,11 @@ private: case IrOpcode::SelectU32: case IrOpcode::SelectF32: return Contains(value.Argument(1)) && Contains(value.Argument(2)); + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPMinTri32: + case IrOpcode::FPMaxTri32: + case IrOpcode::FPMedTri32: case IrOpcode::Phi: for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { if (!Contains(value.Argument(index))) return false; diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp new file mode 100644 index 0000000000..a580617abd --- /dev/null +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -0,0 +1,276 @@ +#include "Optimization/RedundantValueEliminator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +namespace { + +bool Recomputable(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::BitCastU16F16: + case IrOpcode::BitCastF16U16: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::ConvertU16U32: + case IrOpcode::ConvertU32U16: + case IrOpcode::ConvertU8U32: + case IrOpcode::ConvertU32U8: + case IrOpcode::ConvertF32F16: + case IrOpcode::ConvertF16F32: + case IrOpcode::ConvertS32F32: + case IrOpcode::ConvertU32F32: + case IrOpcode::ConvertF32S32: + case IrOpcode::ConvertF32U32: + case IrOpcode::CompositeConstructU64: + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::CompositeConstructU32x3: + case IrOpcode::CompositeConstructF32x2: + case IrOpcode::CompositeConstructU32x4: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::CompositeExtractU32x3: + case IrOpcode::CompositeExtractU32x4: + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::FPSaturate32: + case IrOpcode::BitFieldInsert: + case IrOpcode::BitFieldUExtract: + case IrOpcode::BitFieldSExtract: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: + case IrOpcode::SelectU32: + case IrOpcode::IAdd32: + case IrOpcode::ISub32: + case IrOpcode::IMul32: + case IrOpcode::IAdd64: + case IrOpcode::ISub64: + case IrOpcode::IMul64: + case IrOpcode::SMulHi: + case IrOpcode::UMulHi: + case IrOpcode::IAbs32: + case IrOpcode::ShiftLeftLogical32: + case IrOpcode::ShiftLeftLogical64: + case IrOpcode::ShiftRightLogical32: + case IrOpcode::ShiftRightLogical64: + case IrOpcode::ShiftRightArithmetic32: + case IrOpcode::ShiftRightArithmetic64: + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseAnd64: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + case IrOpcode::BitwiseNot32: + case IrOpcode::BitReverse32: + case IrOpcode::BitCount32: + case IrOpcode::BitCount64: + case IrOpcode::FindUMsb32: + case IrOpcode::FindUMsb64: + case IrOpcode::FindILsb32: + case IrOpcode::SMin32: + case IrOpcode::UMin32: + case IrOpcode::SMax32: + case IrOpcode::UMax32: + case IrOpcode::SLessThan32: + case IrOpcode::SLessThan64: + case IrOpcode::ULessThan32: + case IrOpcode::ULessThan64: + case IrOpcode::IEqual32: + case IrOpcode::IEqual64: + case IrOpcode::SLessThanEqual32: + case IrOpcode::ULessThanEqual32: + case IrOpcode::SGreaterThan32: + case IrOpcode::UGreaterThan32: + case IrOpcode::UGreaterThan64: + case IrOpcode::INotEqual32: + case IrOpcode::INotEqual64: + case IrOpcode::SGreaterThanEqual32: + case IrOpcode::UGreaterThanEqual32: + case IrOpcode::LogicalOr: + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalXor: + case IrOpcode::LogicalNot: + case IrOpcode::FPOrdEqual32: + case IrOpcode::FPUnordEqual32: + case IrOpcode::FPOrdNotEqual32: + case IrOpcode::FPUnordNotEqual32: + case IrOpcode::FPOrdLessThan32: + case IrOpcode::FPUnordLessThan32: + case IrOpcode::FPOrdGreaterThan32: + case IrOpcode::FPUnordGreaterThan32: + case IrOpcode::FPOrdLessThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: + case IrOpcode::FPIsNan32: + case IrOpcode::FPAdd32: + case IrOpcode::FPSub32: + case IrOpcode::FPFma32: + case IrOpcode::FPMad32: + case IrOpcode::FPMul32: + case IrOpcode::FPMin32: + case IrOpcode::FPMax32: + case IrOpcode::FPFloor32: + case IrOpcode::FPCeil32: + case IrOpcode::FPTrunc32: + case IrOpcode::FPFract32: + case IrOpcode::FPRoundEven32: + return true; + default: + return false; + } +} + +struct Operand { + const IrValue* value; + IrType type; + std::uint64_t immediate; + + bool operator==(const Operand& other) const = default; +}; + +struct ValueKey { + IrOpcode opcode; + IrType type; + std::uint64_t flags; + std::vector operands; + + bool operator==(const ValueKey& other) const = default; +}; + +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) { + 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; + std::vector> stack{{&entry, 0u}}; + visited[&entry] = true; + while (!stack.empty()) { + auto& [block, next] = stack.back(); + if (next < block->Successors().size()) { + IrBlock* successor = block->Successors()[next++]; + if (visited.try_emplace(successor, true).second) stack.emplace_back(successor, 0u); + continue; + } + order.push_back(block); + stack.pop_back(); + } + return {order.rbegin(), order.rend()}; +} + +std::vector> DominatorChildren(const std::vector& order, std::unordered_map& index) { + for (std::size_t position = 0; position < order.size(); ++position) index[order[position]] = position; + constexpr std::size_t none = static_cast(-1); + std::vector dominator(order.size(), none); + dominator[0] = 0; + const auto intersect = [&](std::size_t left, std::size_t right) { + while (left != right) { + while (left > right) left = dominator[left]; + while (right > left) right = dominator[right]; + } + return left; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 1; position < order.size(); ++position) { + std::size_t candidate = none; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end() || dominator[found->second] == none) continue; + candidate = candidate == none ? found->second : intersect(candidate, found->second); + } + if (candidate != dominator[position]) { + dominator[position] = candidate; + changed = true; + } + } + } + std::vector> children(order.size()); + for (std::size_t position = 1; position < order.size(); ++position) children[dominator[position]].push_back(order[position]); + return children; +} + +} + +RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { + RedundantValueEliminationStats stats; + 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))); + const auto [found, inserted] = available.try_emplace(key, inst); + if (inserted) { + added.push_back(&found->first); + continue; + } + inst->ReplaceAllUsesWith(found->second); + replacements.emplace(inst, found->second); + ++stats.replacedValues; + } + }; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::vector> stack{{0u, 0u}}; + std::vector scopes{added.size()}; + visit(*order[0]); + while (!stack.empty()) { + auto& [position, next] = stack.back(); + if (next < children[position].size()) { + const auto child = index.at(children[position][next++]); + scopes.push_back(added.size()); + visit(*order[child]); + stack.emplace_back(child, 0u); + continue; + } + while (added.size() > scopes.back()) { + available.erase(available.find(*added.back())); + added.pop_back(); + } + scopes.pop_back(); + stack.pop_back(); + } + const auto replaced = [&](IrValue*& value) { + if (value == nullptr) return; + if (const auto found = replacements.find(value->Resolve()); found != replacements.end()) value = found->second; + }; + for (auto& info : program.Metadata().blockInfo) { + replaced(info.condition); + replaced(info.indirectTarget); + } + for (auto*& read : program.Metadata().dynamicReads) replaced(read); + return stats; +} + +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ce23f6fb6c..37da1a30cf 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -586,6 +586,13 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho void materializeTables(const IrResourcePlan& plan, ResourceSnapshot& snapshot, const std::vector& tables) { for (std::uint32_t i = 0u; i < plan.info.buffers.size(); ++i) { + const auto& buffer = plan.info.buffers[i]; + const bool scalarOnly = buffer.scalar && !buffer.written && !buffer.atomic && !buffer.formatted && buffer.imageAlias == BufferResource::NoImageAlias + && std::none_of(plan.memoryInfo.begin(), plan.memoryInfo.end(), [i](const MemoryInfo& memory) { return memory.kind == ResourceKind::Buffer && memory.resource == i; }); + if (scalarOnly) { + snapshot.buffers.at(i).dwords[1] &= ~0x40000000u; + snapshot.buffers.at(i).dwords[3] = 0u; + } const auto decoded = decodeBufferDescriptor(snapshot.buffers.at(i)); if (decoded.Type() != 0u) throw std::runtime_error("buffer descriptor uses an unsupported type"); if (plan.stage != IrShaderStage::Compute && decoded.AddTid()) throw std::runtime_error("buffer ADD_TID is only valid for compute shaders"); @@ -643,7 +650,9 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2D && image.fmaskCompatible && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { + 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; + if ((volumeLoad || volumeStore) && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 675f7d561f..ee4eaee5f8 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -358,9 +358,17 @@ private: return true; } - static bool usesOnly(const IrValue& value, std::span users) { + static bool usedOnlyAsImage(const IrValue& value, const std::array& reads) { return !value.Uses().empty() && std::ranges::all_of(value.Uses(), [&](const IrValue* user) { - return std::ranges::find(users, user) != users.end(); + if (user->Opcode() != IrOpcode::GetImageResource || user->ArgumentCount() != reads.size()) { + return false; + } + for (std::uint32_t dword = 0; dword < reads.size(); dword++) { + if (user->Argument(dword)->Resolve() != reads[dword]) { + return false; + } + } + return true; }); } @@ -535,9 +543,8 @@ private: } entryOffset += immediateOffset; - const std::array imageUsers {&handle}; for (const auto* read : heapReads) { - if (!usesOnly(*read, imageUsers)) { + if (!usedOnlyAsImage(*read, heapReads)) { return false; } } @@ -1156,15 +1163,6 @@ private: DescriptorSource descriptor; MakeSource(*handle, width, sampler, sampleAdjust, descriptor); std::uint32_t badDword = 0; - if (expected == IrOpcode::GetImageResource) { - for (; badDword < descriptor.dwordCount; badDword++) { - const IrValue* value2 = descriptor.dwords[badDword]->Resolve(); - if (value2->Opcode() == IrOpcode::ReadConstBuffer) { - fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); - } - } - badDword = 0; - } if (!ValidateSource(descriptor, badDword)) { fail(std::string(IrOpcodeName(expected)) + " dword " + std::to_string(badDword) + " is not a valid runtime value; chain: " + describeValueChain(descriptor.dwords[badDword], 8u)); } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 419c219764..6ded41848d 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -236,7 +236,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput()) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index b25f9fe61c..5ca2dc0cc6 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -69,39 +70,169 @@ bool IsLaneId(const IrValue* value) { return value != nullptr && value->Resolve()->Opcode() == IrOpcode::LaneId; } -IrValue* BallotSource(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - if (value->Opcode() != IrOpcode::CompositeExtractU32x4 || !IsImmediate(value->Argument(1), word)) return nullptr; - IrValue* ballot = value->Argument(0)->Resolve(); - return ballot->Opcode() == IrOpcode::Ballot ? ballot->Argument(0)->Resolve() : nullptr; -} - bool IsTrue(const IrValue* value) { return value != nullptr && value->HasImmediate() && value->Type() == IrType::Bool && value->ImmediateBool(); } -IrValue* HostLaneWord(IrValue* raw, std::uint32_t word) { - IrValue* value = raw->Resolve(); - while (value->Opcode() == IrOpcode::BitwiseAnd32 && IsTrue(BallotSource(value->Argument(1), word))) value = value->Argument(0)->Resolve(); - return value; +using LaneWords = std::pair; + +IrValue* BallotPredicate(IrValue* word, std::uint32_t half) { + IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && IsImmediate(word->Argument(1), half) ? ballot->Argument(0)->Resolve() : nullptr; } -IrValue* SavedLaneBit(IrValue* value) { - if (value->Opcode() != IrOpcode::INotEqual32) return nullptr; - IrValue* masked = VariableOperand(value, 0u); - if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return nullptr; - IrValue* shifted = VariableOperand(masked, 1u); - if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return nullptr; - IrValue* bit = shifted->Argument(1)->Resolve(); - if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return nullptr; - IrValue* word = shifted->Argument(0)->Resolve(); - if (word->Opcode() != IrOpcode::SelectU32) return BallotSource(HostLaneWord(word, 0u), 0u); - IrValue* low = word->Argument(0)->Resolve(); - if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return nullptr; - IrValue* source = BallotSource(HostLaneWord(word->Argument(1), 0u), 0u); - return source != nullptr && BallotSource(HostLaneWord(word->Argument(2), 1u), 1u) == source ? source : nullptr; +bool SamePredicate(const IrValue* lhs, const IrValue* rhs) { + if (lhs == nullptr || rhs == nullptr) return false; + if (lhs == rhs) return true; + return lhs->HasImmediate() && rhs->HasImmediate() && lhs->Type() == IrType::Bool && rhs->Type() == IrType::Bool && lhs->ImmediateBool() == rhs->ImmediateBool(); } +std::optional LaneBitWords(IrValue* value, std::uint32_t waveSize) { + if (value->Opcode() != IrOpcode::INotEqual32) return std::nullopt; + IrValue* masked = VariableOperand(value, 0u); + if (masked == nullptr || masked->Opcode() != IrOpcode::BitwiseAnd32) return std::nullopt; + IrValue* shifted = VariableOperand(masked, 1u); + if (shifted == nullptr || shifted->Opcode() != IrOpcode::ShiftRightLogical32) return std::nullopt; + IrValue* bit = shifted->Argument(1)->Resolve(); + if (bit->Opcode() != IrOpcode::BitwiseAnd32 || !IsLaneId(VariableOperand(bit, 31u))) return std::nullopt; + IrValue* word = shifted->Argument(0)->Resolve(); + if (waveSize != 64u) return LaneWords{word, nullptr}; + if (word->Opcode() != IrOpcode::SelectU32) return LaneWords{word, word}; + IrValue* low = word->Argument(0)->Resolve(); + if (low->Opcode() != IrOpcode::ULessThan32 || !IsLaneId(low->Argument(0)) || !IsImmediate(low->Argument(1), 32u)) return LaneWords{word, word}; + return LaneWords{word->Argument(1)->Resolve(), word->Argument(2)->Resolve()}; +} + +class LaneBits { +public: + explicit LaneBits(IrProgram& program) : program(program) {} + + IrValue* Of(const LaneWords& words) { + std::vector visited; + if (matches(words, visited)) return &build(words); + for (const auto& pair : visited) { + if (known.at(pair)) known.erase(pair); + } + return nullptr; + } + +private: + static LaneWords argument(const LaneWords& words, std::size_t index) { + return {words.first->Argument(index)->Resolve(), words.second == nullptr ? nullptr : words.second->Argument(index)->Resolve()}; + } + + bool matches(const LaneWords& words, std::vector& visited) { + if (const auto found = known.find(words); found != known.end()) return found->second; + known.emplace(words, true); + visited.push_back(words); + const bool result = check(words, visited); + if (!result) known[words] = false; + return result; + } + + bool check(const LaneWords& words, std::vector& visited) { + const auto [low, high] = words; + if (low->HasImmediate()) { + if (low->Type() != IrType::U32 || (low->ImmediateU32() != 0u && low->ImmediateU32() != 0xffffffffu)) return false; + return high == nullptr || (high->HasImmediate() && high->Type() == IrType::U32 && high->ImmediateU32() == low->ImmediateU32()); + } + if (high != nullptr && (high->HasImmediate() || high->Opcode() != low->Opcode())) return false; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: { + if (BallotPredicate(low, 0u) == nullptr) return false; + return high == nullptr || SamePredicate(BallotPredicate(low, 0u), BallotPredicate(high, 1u)); + } + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: + return matches(argument(words, 0), visited) && matches(argument(words, 1), visited); + case IrOpcode::BitwiseNot32: + return matches(argument(words, 0), visited); + case IrOpcode::SelectU32: + if (high != nullptr && high->Argument(0)->Resolve() != low->Argument(0)->Resolve()) return false; + return matches(argument(words, 1), visited) && matches(argument(words, 2), visited); + case IrOpcode::Phi: + if (low->ArgumentCount() == 0 || low->ArgumentCount() != low->PhiBlockCount()) return false; + if (high != nullptr && (high->Parent() != low->Parent() || high->ArgumentCount() != low->ArgumentCount() || high->PhiBlockCount() != low->PhiBlockCount())) return false; + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) { + if (high != nullptr && high->PhiBlock(index) != low->PhiBlock(index)) return false; + if (!matches(argument(words, index), visited)) return false; + } + return true; + default: + return false; + } + } + + IrValue& boolean(bool value) { + IrValue& result = program.CreateValue(IrOpcode::Void, IrType::Bool); + result.SetImmediateBool(value); + return result; + } + + IrValue& emit(IrValue* position, IrOpcode opcode, std::initializer_list arguments) { + IrValue& value = program.CreateValue(opcode, IrType::Bool); + for (IrValue* argument : arguments) value.AddArgument(argument); + position->Parent()->InsertInstructionBefore(position, &value); + return value; + } + + IrValue& combine(IrValue* position, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + for (const auto& [fixed, other] : {std::pair{&lhs, &rhs}, std::pair{&rhs, &lhs}}) { + if (!fixed->HasImmediate()) continue; + if (opcode == IrOpcode::LogicalAnd) return fixed->ImmediateBool() ? *other : *fixed; + if (opcode == IrOpcode::LogicalOr) return fixed->ImmediateBool() ? *fixed : *other; + if (!fixed->ImmediateBool()) return *other; + } + return emit(position, opcode, {&lhs, &rhs}); + } + + IrValue& build(const LaneWords& words) { + IrValue* low = words.first; + if (low->HasImmediate()) return boolean(low->ImmediateU32() != 0u); + if (const auto found = built.find(words); found != built.end()) return *found->second; + IrValue* result = nullptr; + switch (low->Opcode()) { + case IrOpcode::CompositeExtractU32x4: + result = BallotPredicate(low, 0u); + break; + case IrOpcode::BitwiseAnd32: + case IrOpcode::BitwiseOr32: + case IrOpcode::BitwiseXor32: { + const auto opcode = low->Opcode() == IrOpcode::BitwiseAnd32 ? IrOpcode::LogicalAnd : low->Opcode() == IrOpcode::BitwiseOr32 ? IrOpcode::LogicalOr : IrOpcode::LogicalXor; + IrValue& lhs = build(argument(words, 0)); + IrValue& rhs = build(argument(words, 1)); + result = &combine(low, opcode, lhs, rhs); + break; + } + case IrOpcode::BitwiseNot32: { + IrValue& operand = build(argument(words, 0)); + result = operand.HasImmediate() ? &boolean(!operand.ImmediateBool()) : &emit(low, IrOpcode::LogicalNot, {&operand}); + break; + } + case IrOpcode::SelectU32: { + IrValue& chosen = build(argument(words, 1)); + IrValue& other = build(argument(words, 2)); + result = &emit(low, IrOpcode::SelectU1, {low->Argument(0)->Resolve(), &chosen, &other}); + break; + } + default: { + IrValue& phi = program.CreateValue(IrOpcode::Phi, IrType::Bool); + low->Parent()->InsertInstructionBefore(low, &phi); + built.emplace(words, &phi); + for (std::size_t index = 0; index < low->ArgumentCount(); ++index) phi.AddPhiOperand(low->PhiBlock(index), &build(argument(words, index))); + return phi; + } + } + built.emplace(words, result); + return *result; + } + + IrProgram& program; + std::map known; + std::map built; +}; + bool Implies(IrValue* rawPredicate, IrValue* rawCondition) { IrValue* predicate = rawPredicate->Resolve(); IrValue* condition = rawCondition->Resolve(); @@ -420,17 +551,14 @@ public: : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : static_cast(tessellation.patchesPerGroup)) {} void Run() { - for (IrBlock* block : program.BlockOrder()) { - for (IrValue* inst : block->Instructions()) { - if (IrValue* source = SavedLaneBit(inst)) inst->ReplaceAllUsesWith(source); - } - } + if (stage == IrShaderStage::TessellationControl) lowerOwnLaneBits(); collect(); derive(); for (const auto& access : accesses) rewrite(access); if (stage == IrShaderStage::TessellationEvaluation) removePrimitiveExports(); constexpr DeadCodeEliminator deadCodeEliminator; deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::TessellationControl) removeUnobservedCycles(); std::vector relative; for (IrBlock* block : program.BlockOrder()) { for (IrValue* inst : block->Instructions()) { @@ -444,8 +572,9 @@ public: Fail("a tessellation ring base is used outside a lowered ring access", 0u); } } - requireRelativeIndependence(relative); - for (IrValue* base : relative) base->ReplaceAllUsesWith(&constant(0u)); + const bool data = usedAsData(relative); + if (data) requireGroupedPatches(); + for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; @@ -453,6 +582,20 @@ public: } private: + void lowerOwnLaneBits() { + LaneBits laneBits(program); + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + const auto words = LaneBitWords(inst, program.WaveSize()); + if (!words) continue; + if (IrValue* bit = laneBits.Of(*words)) inst->ReplaceUsesWith(bit, true); + } + } + ResolveControlFlowIdentities(program); + constexpr DeadCodeEliminator identities; + identities.RemoveIdentities(program); + } + void collect() { const auto& memoryInfo = program.Resources().memoryInfo; for (IrBlock* block : program.BlockOrder()) { @@ -513,11 +656,16 @@ private: void derive() { const auto inputs = static_cast(tessellation.inputControlPoints); const auto outputs = static_cast(tessellation.outputControlPoints); + const auto soleControlPoint = [&](const Access& access) { + const auto end = access.address.constant + 4 * static_cast(access.components); + return outputs == 1 && access.address.vertex == 0 && access.address.relative > 0 && access.address.constant >= 0 && end <= access.address.relative; + }; for (auto& access : accesses) { if (access.kind == TessellationAttribute::LocalOutput) agree(localStride, access.address.relative, access.pc); if (access.kind == TessellationAttribute::ControlInput && access.address.vertex != 0) agree(localStride, access.address.vertex, access.pc); - if (access.kind == TessellationAttribute::ControlInput && access.address.dynamic && access.address.relative > 0 && access.address.relative % inputs == 0) agree(localStride, access.address.relative / inputs, access.pc); + 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::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) { @@ -549,7 +697,7 @@ private: if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % localStride != 0)) Fail("the hull shader reads LDS at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::ControlOutput: - if (address.vertex == 0) { + if (address.vertex == 0 && !soleControlPoint(access)) { if (controlStride == 0u || evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc); record(access, end); access.kind = TessellationAttribute::PatchOutput; @@ -590,7 +738,7 @@ private: return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; } - void requireRelativeIndependence(const std::vector& bases) { + bool usedAsData(const std::vector& bases) { std::vector pending(bases.begin(), bases.end()); std::unordered_set visited(bases.begin(), bases.end()); while (!pending.empty()) { @@ -616,12 +764,65 @@ private: } if (dynamicInputAddress(user, value)) continue; const auto address = evaluator.Evaluate(value, nullptr); - if (!address || address->relative != 0) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (!address || address->relative != 0) return true; } } + return false; + } + + void requireGroupedPatches() const { + if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); + if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); + if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + } + + IrValue& groupedPatch(IrValue& base) { + const auto emit = [&](IrOpcode opcode, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, IrType::U32); + for (IrValue* argument : arguments) value.AddArgument(argument); + base.Parent()->InsertInstructionBefore(&base, &value); + return value; + }; + const auto remainder = [&](IrValue& value, std::uint32_t divisor) -> IrValue& { + IrValue& count = constant(divisor); + return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); + }; + IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); + } + + void removeUnobservedCycles() { + std::unordered_set observed; + std::vector pending; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->MayHaveSideEffects() && observed.insert(inst).second) pending.push_back(inst); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (argument != nullptr && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (!observed.contains(inst)) unobserved.push_back(inst); + } + } + for (IrValue* inst : unobserved) { + for (std::size_t index = 0; index < inst->ArgumentCount(); ++index) inst->ReplaceArgument(index, nullptr); + } + for (IrValue* inst : unobserved) { + inst->Parent()->RemoveInstruction(inst); + inst->Invalidate(); + } } void requireFirstInvocation(const Access& access) { + if (tessellation.outputControlPoints == 1u) return; std::unordered_set path; if (!FirstInvocationOnly(access.predicate, evaluator, path)) Fail("per-patch tessellation output is not written by control point 0 alone", access.pc); } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp index a03a46841e..9456f38cd1 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp @@ -8,6 +8,9 @@ namespace ShaderRecompiler { +inline constexpr std::uint32_t SharedApertureHigh = 0x80000000u; +inline constexpr std::uint32_t PrivateApertureHigh = 0x70000000u; + class RdnaInstructionDecoder { public: [[nodiscard]] RdnaProgram Decode(std::span code) const; @@ -23,6 +26,7 @@ void DecodeRdnaProgram(std::span code, RdnaProgram& program RdnaOperand DecodeRdnaScalarSource(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaScalarDestination(std::uint32_t code, std::uint32_t programCounter); RdnaOperand DecodeRdnaVectorGpr(std::uint32_t reg); +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand); void ReadRdnaLiteralOperands(std::span code, std::uint32_t wordIndex, RdnaInstruction& instruction); void SetRdnaRawWords(RdnaInstruction& instruction, std::span code, std::uint32_t wordIndex, std::uint32_t wordCount); void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily family, std::uint32_t opcodeId, const char* reason); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 04f2b9e074..c3d1a68701 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,7 +13,7 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, true, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, @@ -40,7 +40,9 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format11_11_10Float, true, false, false}, {IrBufferFormat::Format10_11_11Float, true, true, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, + {IrBufferFormat::Format10_10_10_2SNorm, true, false, false}, {IrBufferFormat::Format10_10_10_2UInt, true, true, false}, + {IrBufferFormat::Format10_10_10_2SInt, true, false, true}, {IrBufferFormat::Format8_8_8_8UNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8SNorm, true, false, false}, {IrBufferFormat::Format8_8_8_8UInt, true, true, false}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index 0ab4c4645b..f64c3c6bd7 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -417,7 +417,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 16u) & 31u) * 4u; const auto sampler = ((word1 >> 21u) & 31u) * 4u; const bool r128 = (word0 & 0x8000u) != 0u; - if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler + 4u > 106u)) { + if (resource + (r128 ? 4u : 8u) > 106u || ((info.sample || info.gather || opcode == 0x60u) && sampler > 104u)) { throw std::runtime_error("MIMG descriptor register range overflow"); } RdnaInstruction instruction{}; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp index fc5e2b9d63..abdb151c23 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp @@ -300,6 +300,16 @@ void SetRdnaUnsupported(RdnaInstruction& instruction, RdnaInstructionFamily fami throw std::invalid_argument("unsupported RDNA instruction, family=" + std::to_string(static_cast(family)) + " opcode=" + toHexString(opcodeId) + " reason=" + reason + " pc=" + toHexString(instruction.programCounter)); } +std::uint64_t RdnaApertureSourceValue(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::SrcSharedBase: return std::uint64_t{SharedApertureHigh} << 32u; + case RdnaOperandKind::SrcSharedLimit: return (std::uint64_t{SharedApertureHigh} << 32u) | 0xffffffffu; + case RdnaOperandKind::SrcPrivateBase: return std::uint64_t{PrivateApertureHigh} << 32u; + case RdnaOperandKind::SrcPrivateLimit: return (std::uint64_t{PrivateApertureHigh} << 32u) | 0xffffffffu; + default: throw std::invalid_argument("RdnaApertureSourceValue: " + RdnaOperandToString(operand) + " is not a scalar aperture source"); + } +} + std::string RdnaOperandToString(const RdnaOperand& operand) { switch (operand.kind) { case RdnaOperandKind::None: return "none"; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 1d9c25efe9..dd297de461 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1126,6 +1126,9 @@ constexpr Vop1SdwaRule vop1SdwaRules[] = { {RdnaOpcode::VCvtF32U32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32I32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32Ubyte0, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte1, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte2, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VCvtF32Ubyte3, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VNotB32, sdwaSelAll(), sdwaSelBytes() | sdwaSelWords(), sdwaSelAll(), false}, {RdnaOpcode::VFfblB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VFfbhU32, sdwaSelAll(), 0, 0, false}, @@ -2090,7 +2093,8 @@ bool isVop3F16InterpolationOpcode(RdnaOpcode opcode) { void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { const bool hasSource2 = instruction.op != RdnaOpcode::VInterpP1llF16; const bool f16Result = instruction.op == RdnaOpcode::VInterpP2F16; - if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || opSel != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { + const std::uint32_t resultHalf = f16Result ? 0x8u : 0x0u; + if ((abs & 0x1u) != 0u || (neg & 0x1u) != 0u || (opSel & ~resultHalf) != 0u || (f16Result && omod != 0u) || (!hasSource2 && ((abs | neg) & 0x4u) != 0u)) { throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); } if (src1 < 256u || (hasSource2 && src2 < 256u)) { @@ -2100,6 +2104,7 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst instruction.destination = DecodeRdnaVectorGpr(vdst); instruction.destination.clamp = clamp != 0u; instruction.destination.omod = omod; + if ((opSel & resultHalf) != 0u) instruction.destination.sdwaSel = 5u; instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); instruction.source0.absolute = (abs & 0x2u) != 0u; instruction.source0.negate = (neg & 0x2u) != 0u; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index aacd8989dd..1b83081ad7 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -32,6 +32,7 @@ #include "Optimization/include/Optimization/HostInterpolationChecker.hpp" #include "Optimization/include/Optimization/MaskedSelectEliminator.hpp" #include "Optimization/include/Optimization/ReadLaneEliminator.hpp" +#include "Optimization/include/Optimization/RedundantValueEliminator.hpp" #include "Optimization/include/Optimization/RequestMemoryView.hpp" #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" @@ -117,6 +118,8 @@ std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } +std::atomic frontEndBuilds{0}; + } struct PreparedControlFlow { @@ -156,13 +159,26 @@ std::shared_ptr ShaderPreparationContext::AcquireFron return prepared; } +std::uint64_t FrontEndBuilds() { + return frontEndBuilds.load(std::memory_order_relaxed); +} + +std::uint32_t ShaderPreparationContext::ResourcePrograms() const { + return resourcePrograms; +} + IrProgram PrepareResourceProgram(const RecompileRequest& request) { return PrepareResourceProgram(request, nullptr); } IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) { + frontEndBuilds.fetch_add(1, std::memory_order_relaxed); ShaderPreparationContext local; - const auto frontend = (preparation != nullptr ? *preparation : local).AcquireFrontend(request); + auto& context = preparation != nullptr ? *preparation : local; + ++context.resourcePrograms; + context.interfaceSource.reset(); + context.interfaceProgram.reset(); + const auto frontend = context.AcquireFrontend(request); const auto stageKind = toShaderStageKind(request.shader.stage); const auto inputInfo = RequestInputInfo(request); const auto& decoded = frontend->decoded; @@ -288,7 +304,7 @@ struct EmissionFailure { std::exception_ptr failure; }; -struct SourceEntry { +struct SourceEntry : std::enable_shared_from_this { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is // accepted only when its code matches word for word. Owned here because the request's span @@ -357,6 +373,14 @@ bool FailureMemo() { return memo; } +bool diskPlans() { + static const bool enabled = [] { + const char* dumpIr = std::getenv("APS5_DUMP_IR"); + return std::getenv("APS5_NO_PERF_DISK_PLANS") == nullptr && (dumpIr == nullptr || *dumpIr == '\0'); + }(); + return enabled && ShaderDiskCache::Enabled() && !DebugProbeActive(); +} + std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) { static PrimedSharedMutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. @@ -400,13 +424,26 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr std::lock_guard lock(source->mutex); if (source->plan == nullptr) { if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); - try { - ResourceProgram resource(request, preparation, reclaim); - source->plan = std::move(resource.plan); - source->program = std::move(resource.program); - } catch (...) { - if (FailureMemo()) source->planFailure = std::current_exception(); - throw; + const bool disk = diskPlans(); + std::vector planKey; + if (disk) { + ShaderDiskCache::BuildPlanKey(request, HostSubgroupSize(request), planKey); + IrResourcePlan loaded; + if (ShaderDiskCache::LoadPlan(planKey, loaded)) { + loaded.shaderHash = request.shader.codeAddress; + source->plan = std::make_shared(std::move(loaded)); + } + } + if (source->plan == nullptr) { + try { + ResourceProgram resource(request, preparation, reclaim); + source->plan = std::move(resource.plan); + source->program = std::move(resource.program); + } catch (...) { + if (FailureMemo()) source->planFailure = std::current_exception(); + throw; + } + if (disk) ShaderDiskCache::StorePlan(std::move(planKey), source->plan); } } } @@ -422,7 +459,19 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) try { +template +auto recordEmissionFailure(std::exception_ptr* emissionFailure, TStep&& step) -> decltype(step()) { + try { + return step(); + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } +} + +void prepareInterface(const RecompileRequest& request, IrProgram& program) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -430,13 +479,19 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets); deadCodeEliminator.RemoveIdentities(program); + constexpr RedundantValueEliminator redundantValueEliminator; + static_cast(redundantValueEliminator.Eliminate(program)); deadCodeEliminator.Eliminate(program); constexpr ShaderInfoCollector shaderInfoCollector; shaderInfoCollector.Collect(program, inputInfo); +} +CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) { + const auto inputInfo = RequestInputInfo(request); constexpr BindingAllocator bindingAllocator; bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment); + program.Metadata().bindingLayoutComplete = false; auto bindings = bindingAllocator.Allocate(program, request.layout, registration); SpirvTargetOptions targetOptions {}; @@ -520,12 +575,16 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra } #endif - return {request.layout, std::move(program).TakeCompiledInfo(), std::move(static_cast(bindings)), std::move(result)}; -} catch (const std::bad_alloc&) { - throw; -} catch (...) { - if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); - throw; + return {request.layout, {}, std::move(static_cast(bindings)), std::move(result)}; +} + +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) { + return recordEmissionFailure(emissionFailure, [&] { + prepareInterface(request, program); + auto variant = emitVariant(request, program); + variant.info = std::move(program).TakeCompiledInfo(); + return variant; + }); } struct SpecializedModule { @@ -897,6 +956,29 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes; } +} + +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) { + preparation.interfaceSource.reset(); + preparation.interfaceProgram.reset(); + auto program = source.program != nullptr ? std::move(source.program) : std::make_unique(PrepareResourceProgram(request, &preparation)); + recordEmissionFailure(emissionFailure, [&] { prepareInterface(request, *program); }); + preparation.interfaceSource = source.shared_from_this(); + preparation.interfaceProgram = std::move(program); + } + auto& program = *preparation.interfaceProgram; + return recordEmissionFailure(emissionFailure, [&] { + auto variant = emitVariant(request, program); + variant.info = program.CompiledInfo(); + return variant; + }); + } +}; + +namespace { + std::shared_ptr findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, bool& cacheHit, ShaderPreparationContext* preparation = nullptr) { for (const auto& candidate : source.variants) { if (sameLayout(candidate->layout, request.layout)) { @@ -921,16 +1003,24 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, } } if (variant == nullptr) { - auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation); - source.program.reset(); + static const bool layoutPair = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr; std::exception_ptr emissionFailure; try { - variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + if (layoutPair && preparation != nullptr) { + variant = std::make_shared(PreparedInterface::Compile(source, request, *preparation, &emissionFailure)); + } else { + auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation); + source.program.reset(); + variant = std::make_shared(compileVariant(request, std::move(program), &emissionFailure)); + } } catch (...) { if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, emissionFailure}); throw; } - if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); + if (disk) { + if (auto persisted = ShaderDiskCache::Persist(diskKey, *variant)) variant = std::move(persisted); + else ShaderDiskCache::Store(std::move(diskKey), variant); + } } source.program.reset(); source.variants.push_back(variant); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9a22de5259..f17e0ad63b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -3,6 +3,7 @@ #include "RuntimeAbi.hpp" #include "PipelineSpecialization.hpp" +#include #include #include #include @@ -151,6 +152,7 @@ struct ShaderPixelStageInfo { bool stencilRefExportEnable; RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; bool layerExported = false; + bool ditheredAlphaToCoverage = false; }; struct ShaderVertexBufferResource { @@ -289,6 +291,8 @@ struct TessellationConfiguration { std::uint32_t partitioning; std::uint32_t outputTopology; std::uint32_t patchesPerGroup = 256; + std::uint32_t primitiveGroupSize = 0; + bool breakAtInstance = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; @@ -434,39 +438,43 @@ class SharedSpirv { public: SharedSpirv() = default; SharedSpirv(std::vector words) : words(std::make_shared>(std::move(words))) {} + SharedSpirv(std::shared_ptr owner, std::span view) : owner(std::move(owner)), view(view) {} SharedSpirv& operator=(std::vector other) { words = std::make_shared>(std::move(other)); + owner.reset(); + view = {}; return *this; } - [[nodiscard]] const std::vector& Words() const { return words ? *words : Empty(); } - operator const std::vector&() const { return Words(); } + [[nodiscard]] std::span Words() const { return words ? std::span(*words) : view; } + [[nodiscard]] std::vector Copy() const { return {Words().begin(), Words().end()}; } [[nodiscard]] std::size_t size() const { return Words().size(); } [[nodiscard]] bool empty() const { return Words().empty(); } [[nodiscard]] const std::uint32_t* data() const { return Words().data(); } [[nodiscard]] std::uint32_t* data() { return Mutable().data(); } [[nodiscard]] const std::uint32_t& operator[](std::size_t index) const { return Words()[index]; } [[nodiscard]] std::uint32_t& operator[](std::size_t index) { return Mutable()[index]; } - [[nodiscard]] std::vector::const_iterator begin() const { return Words().begin(); } - [[nodiscard]] std::vector::const_iterator end() const { return Words().end(); } + [[nodiscard]] std::span::iterator begin() const { return Words().begin(); } + [[nodiscard]] std::span::iterator end() const { return Words().end(); } [[nodiscard]] std::vector::iterator begin() { return Mutable().begin(); } [[nodiscard]] std::vector::iterator end() { return Mutable().end(); } void resize(std::size_t count) { Mutable().resize(count); } std::vector::iterator insert(std::vector::const_iterator where, std::initializer_list values) { return Mutable().insert(where, values); } - friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return left.words == right.words || left.Words() == right.Words(); } + friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } private: - static const std::vector& Empty() { - static const std::vector empty; - return empty; - } - // Copy on write: a holder whose words are shared gets its own copy before the first write. std::vector& Mutable() { - if (words == nullptr || words.use_count() != 1) words = std::make_shared>(Words()); + if (words == nullptr || words.use_count() != 1) { + words = std::make_shared>(Copy()); + owner.reset(); + view = {}; + } return *words; } std::shared_ptr> words; + std::shared_ptr owner; + std::span view; }; struct VertexInput { diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index db8e2f8fe1..ee9393e078 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -94,18 +94,42 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span& bytes) { bytes.clear(); try { +#ifdef _WIN32 + const auto file = CreateFileW(path.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 false; + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{file}; + LARGE_INTEGER fileSize{}; + if (!GetFileSizeEx(file, &fileSize)) return false; + const auto size = fileSize.QuadPart; +#else std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) return false; const auto size = static_cast(file.tellg()); +#endif if (size < 0 || static_cast(size) > bytes.max_size() || static_cast(size) > static_cast(std::numeric_limits::max())) return false; +#ifdef _WIN32 + bytes.resize(static_cast(size)); + for (std::size_t offset = 0; offset < bytes.size();) { + const auto left = bytes.size() - offset; + const auto chunk = static_cast(left > MAXDWORD ? MAXDWORD : left); + DWORD count = 0; + if (!ReadFile(file, bytes.data() + offset, chunk, &count, nullptr) || count == 0) { + bytes.clear(); + return false; + } + offset += count; + } +#else if (!file.seekg(0, std::ios::beg)) return false; bytes.resize(static_cast(size)); if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { bytes.clear(); return false; } +#endif return true; } catch (const std::bad_alloc&) { bytes.clear(); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index f25dc30969..355e720e76 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -14,6 +14,12 @@ #elif defined(__APPLE__) #include #endif +#ifndef _WIN32 +#include +#include +#include +#include +#endif #include "ShaderCacheVersion.hpp" #include #include @@ -24,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +38,8 @@ #include #include #include +#include +#include namespace ShaderRecompiler { @@ -74,9 +83,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 352, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 568, "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"); @@ -94,10 +103,56 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 152, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 80, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); +static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(sizeof(IrValue) == 152, "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) == 104, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource::IndirectImage) == 28, "IndirectImage changed: update the plan encoder"); +static_assert(sizeof(ResourceBlock) == 80, "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"); #endif namespace { +struct AnyField { + template + operator TField() const; +}; + +template +constexpr bool bracedWith(std::index_sequence) { + return requires { TStruct{(static_cast(Index), AnyField{})...}; }; +} + +template +constexpr std::size_t fieldCount() { + if constexpr (bracedWith(std::make_index_sequence{})) { + return fieldCount(); + } else { + return Count; + } +} + +static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); +static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); +static_assert(fieldCount() == 37, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 7, "SamplerResource changed: update the info encoder"); +static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); +static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); +static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); +static_assert(fieldCount() == 21, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); +static_assert(fieldCount() == 30, "MemoryInfo changed: update the plan encoder"); +static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); +static_assert(fieldCount() == 7, "IndirectImage changed: update the plan encoder"); +static_assert(fieldCount() == 4, "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"); + constexpr std::uint32_t FileMagic = 0x43535041u; struct FileHeader { @@ -109,6 +164,69 @@ struct FileHeader { std::uint64_t keyHash; std::uint64_t payloadHash; }; + +std::size_t PayloadOffset(std::size_t keyBytes) { + constexpr std::size_t alignment = 8; + return (sizeof(FileHeader) + keyBytes + alignment - 1) / alignment * alignment; +} + +class MappedFile { +public: + MappedFile(const void* data, std::size_t size) : data(data), size(size) {} + MappedFile(const MappedFile&) = delete; + MappedFile& operator=(const MappedFile&) = delete; + ~MappedFile() { +#ifdef _WIN32 + UnmapViewOfFile(data); +#else + munmap(const_cast(data), size); +#endif + } + + [[nodiscard]] std::span Bytes() const { return {static_cast(data), size}; } + +private: + const void* data; + std::size_t size; +}; + +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); + 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()) { + CloseHandle(file); + return nullptr; + } + const HANDLE mapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(file); + if (mapping == nullptr) return nullptr; + const void* data = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0); + CloseHandle(mapping); + if (data == nullptr) return nullptr; + try { + return std::make_shared(data, static_cast(length.QuadPart)); + } catch (...) { + UnmapViewOfFile(data); + throw; + } +#else + const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (descriptor < 0) return nullptr; + struct stat status{}; + void* data = MAP_FAILED; + if (fstat(descriptor, &status) == 0 && status.st_size > 0) data = mmap(nullptr, static_cast(status.st_size), PROT_READ, MAP_PRIVATE, descriptor, 0); + close(descriptor); + if (data == MAP_FAILED) return nullptr; + try { + return std::make_shared(data, static_cast(status.st_size)); + } catch (...) { + munmap(data, static_cast(status.st_size)); + throw; + } +#endif +} static_assert(sizeof(FileHeader) == 48 && std::is_trivially_copyable_v); class Writer { @@ -138,6 +256,14 @@ public: for (const bool flag : flags) Value(flag); } + void Compact(std::uint64_t value) { + while (value >= 0x80u) { + Raw(static_cast(value | 0x80u)); + value >>= 7u; + } + Raw(static_cast(value)); + } + void Text(const std::string& text) { Value(text.size()); const auto bytes = std::as_bytes(std::span(text.data(), text.size())); @@ -163,10 +289,26 @@ private: class Reader { public: - explicit Reader(std::span bytes) : bytes(bytes) {} + explicit Reader(std::span bytes, std::shared_ptr owner = nullptr) : bytes(bytes), owner(std::move(owner)) {} [[nodiscard]] bool Ok() const { return ok; } [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + [[nodiscard]] std::size_t Remaining() const { return ok ? bytes.size() - position : 0; } + void Fail() { ok = false; } + + [[nodiscard]] std::uint64_t Compact() { + std::uint64_t value = 0; + for (std::uint32_t shift = 0; shift < 64u; shift += 7u) { + const auto byte = Raw(); + if (!ok) return 0; + value |= static_cast(byte & 0x7fu) << shift; + if ((byte & 0x80u) != 0u) continue; + if ((byte == 0u && shift != 0u) || (shift == 63u && byte > 1u)) ok = false; + return ok ? value : 0; + } + ok = false; + return 0; + } template void Value(TValue& value) requires (std::is_integral_v || std::is_enum_v) { @@ -198,6 +340,18 @@ public: } } + SharedSpirv Spirv() { + const auto count = Count(sizeof(std::uint32_t)); + const auto* first = bytes.data() + position; + position += count * sizeof(std::uint32_t); + if (owner != nullptr && reinterpret_cast(first) % alignof(std::uint32_t) == 0) { + return SharedSpirv(owner, std::span(reinterpret_cast(first), count)); + } + std::vector words(count); + if (count != 0) std::memcpy(words.data(), first, count * sizeof(std::uint32_t)); + return SharedSpirv(std::move(words)); + } + void Flags(std::vector& flags) { const auto count = Count(1); flags.assign(count, false); @@ -243,6 +397,7 @@ private: } std::span bytes; + std::shared_ptr owner; std::size_t position = 0; bool ok = true; }; @@ -342,9 +497,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.runtimeAbiVersion); - std::vector words; - reader.Values(words); - result.spirv = std::move(words); + result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); @@ -452,15 +605,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) { }); } -void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { - writer.Value(compiled.stage); - writer.Value(compiled.shaderHash); - writer.Value(compiled.waveSize); - writer.Value(compiled.userDataBase); - writer.Value(compiled.userDataCount); - writer.Value(compiled.scratchDwords); - writer.Value(compiled.paramExportMask); - const auto& info = compiled.info; +void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { writer.Value(info.scratchDwords); writer.Value(info.sharedMemoryBytes); writer.List(info.buffers, [](Writer& out, const BufferResource& buffer) { @@ -558,18 +703,9 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.dispatchThreadLimit); writer.Value(info.rasterPrimitive); writer.Value(info.layerExported); - encodeLayout(writer, compiled.bindings); } -void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { - reader.Value(compiled.stage); - reader.Value(compiled.shaderHash); - reader.Value(compiled.waveSize); - reader.Value(compiled.userDataBase); - reader.Value(compiled.userDataCount); - reader.Value(compiled.scratchDwords); - reader.Value(compiled.paramExportMask); - auto& info = compiled.info; +void decodeShaderInfo(Reader& reader, ShaderInfo& info) { reader.Value(info.scratchDwords); reader.Value(info.sharedMemoryBytes); reader.List(info.buffers, 27, [](Reader& in, BufferResource& buffer) { @@ -669,6 +805,29 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.dispatchThreadLimit); reader.Value(info.rasterPrimitive); reader.Value(info.layerExported); +} + +void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { + writer.Value(compiled.stage); + writer.Value(compiled.shaderHash); + writer.Value(compiled.waveSize); + writer.Value(compiled.userDataBase); + writer.Value(compiled.userDataCount); + writer.Value(compiled.scratchDwords); + writer.Value(compiled.paramExportMask); + encodeShaderInfo(writer, compiled.info); + encodeLayout(writer, compiled.bindings); +} + +void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { + reader.Value(compiled.stage); + reader.Value(compiled.shaderHash); + reader.Value(compiled.waveSize); + reader.Value(compiled.userDataBase); + reader.Value(compiled.userDataCount); + reader.Value(compiled.scratchDwords); + reader.Value(compiled.paramExportMask); + decodeShaderInfo(reader, compiled.info); decodeLayout(reader, compiled.bindings); } @@ -684,14 +843,331 @@ void decodeAllocation(Reader& reader, CompiledBindingLayout& allocation) { reader.Value(allocation.pushConstantSizeBytes); } +constexpr std::uint32_t PlanEntryKind = 0x4e414c50u; +constexpr std::uint32_t NoPlanIndex = std::numeric_limits::max(); + +void encodeMemoryInfo(Writer& out, const MemoryInfo& memory) { + out.Value(memory.kind); + out.Value(memory.resource); + out.Value(memory.sampler); + out.Value(memory.offset); + out.Value(memory.secondaryOffset); + out.Value(memory.dmask); + out.Value(memory.dataDwords); + out.Value(memory.dataBits); + out.Value(memory.componentIndex); + out.Value(memory.componentCount); + out.Value(memory.dataFormat); + out.Value(memory.numberFormat); + out.Value(memory.imageSampleFlags); + out.Value(memory.imageDimension); + out.Value(memory.imageAddressComponents); + out.Value(memory.addressIsFull); + out.Value(memory.dataSigned); + out.Value(memory.typed); + out.Value(memory.formatted); + out.Value(memory.d16); + out.Value(memory.imageHasMip); + out.Value(memory.imageR128); + out.Value(memory.imagePacked); + out.Value(memory.imageByElements); + out.Value(memory.idxen); + out.Value(memory.offen); + out.Value(memory.planningOnly); + out.Value(memory.coherent); + out.Value(memory.gpuDescriptor); + out.Value(memory.flushDenormals); +} + +void decodeMemoryInfo(Reader& in, MemoryInfo& memory) { + in.Value(memory.kind); + in.Value(memory.resource); + in.Value(memory.sampler); + in.Value(memory.offset); + in.Value(memory.secondaryOffset); + in.Value(memory.dmask); + in.Value(memory.dataDwords); + in.Value(memory.dataBits); + in.Value(memory.componentIndex); + in.Value(memory.componentCount); + in.Value(memory.dataFormat); + in.Value(memory.numberFormat); + in.Value(memory.imageSampleFlags); + in.Value(memory.imageDimension); + in.Value(memory.imageAddressComponents); + in.Value(memory.addressIsFull); + in.Value(memory.dataSigned); + in.Value(memory.typed); + in.Value(memory.formatted); + in.Value(memory.d16); + in.Value(memory.imageHasMip); + in.Value(memory.imageR128); + in.Value(memory.imagePacked); + in.Value(memory.imageByElements); + in.Value(memory.idxen); + in.Value(memory.offen); + in.Value(memory.planningOnly); + in.Value(memory.coherent); + in.Value(memory.gpuDescriptor); + in.Value(memory.flushDenormals); +} + +bool encodePlan(Writer& writer, const IrResourcePlan& plan) { + if (std::ranges::any_of(plan.valueStorage, [](const auto& value) { return value == nullptr; }) || std::ranges::any_of(plan.blockStorage, [](const auto& block) { return block == nullptr; })) return false; + std::unordered_map values; + std::unordered_map blocks; + values.reserve(plan.valueStorage.size()); + blocks.reserve(plan.blockStorage.size()); + for (std::size_t index = 0; index < plan.valueStorage.size(); ++index) values.emplace(plan.valueStorage[index].get(), static_cast(index)); + for (std::size_t index = 0; index < plan.blockStorage.size(); ++index) blocks.emplace(plan.blockStorage[index].get(), static_cast(index)); + bool owned = values.size() == plan.valueStorage.size() && blocks.size() == plan.blockStorage.size(); + std::uint32_t current = 0; + const auto valueIndex = [&](const IrValue* value) { + if (value == nullptr) return NoPlanIndex; + const auto found = values.find(value); + if (found == values.end()) { + owned = false; + return NoPlanIndex; + } + return found->second; + }; + writer.Value(plan.stage); + writer.Value(plan.shaderHash); + writer.Value(plan.userDataBase); + writer.Value(plan.userDataCount); + writer.Value(plan.srgbDecodeFormats); + writer.List(plan.blockStorage, [](Writer& out, const std::unique_ptr& block) { out.Value(block->Id()); }); + writer.List(plan.valueStorage, [&](Writer& out, const std::unique_ptr& value) { + const auto bank = static_cast(value->Register().bank); + if (bank > 0xfu) owned = false; + out.Compact(static_cast(value->Opcode())); + out.Compact(static_cast(value->Type())); + out.Compact(value->Id()); + out.Compact(value->Flags()); + out.Compact((static_cast(value->Register().index) << 4u) | (bank & 0xfu)); + out.Compact((static_cast(value->ArgumentCount()) << 1u) | (value->HasImmediate() ? 1u : 0u)); + if (value->HasImmediate()) out.Compact(value->ImmediateU64()); + for (const auto* argument : value->Arguments()) { + const auto index = valueIndex(argument); + const auto delta = static_cast(index) - static_cast(current); + out.Compact(argument == nullptr ? 0u : ((static_cast(delta) << 1u) ^ static_cast(delta >> 63)) + 1u); + } + if (value->IsPhi() && value->PhiBlockCount() != value->ArgumentCount()) owned = false; + for (std::size_t operand = 0; value->IsPhi() && operand < value->ArgumentCount(); ++operand) { + const auto found = operand < value->PhiBlockCount() ? blocks.find(value->PhiBlock(operand)) : blocks.end(); + if (found == blocks.end()) owned = false; + out.Compact(found != blocks.end() ? found->second : 0u); + } + ++current; + }); + writer.List(plan.memoryInfo, encodeMemoryInfo); + writer.List(plan.descriptorSources, [&](Writer& out, const DescriptorSource& source) { + for (const auto* dword : source.dwords) out.Value(valueIndex(dword)); + out.Value(source.dwordCount); + const auto table = source.indirectImage.value_or(DescriptorSource::IndirectImage{}); + out.Value(source.indirectImage.has_value()); + out.Value(table.materialSource); + out.Value(table.heapSource); + out.Value(table.selectorStride); + out.Value(table.selectorOffset); + out.Value(table.keyArg); + out.Value(table.entryOffset); + out.Value(table.hasMaterial); + }); + writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { + out.Value(valueIndex(block.condition)); + out.Values(std::span(block.successors)); + out.Values(std::span(block.sources)); + out.Values(std::span(block.images)); + }); + writer.Values(std::span(plan.materializationSources)); + writer.List(plan.srtReads, [&](Writer& out, const SrtRead& read) { + out.Value(valueIndex(read.value)); + out.Value(read.flatOffset); + }); + writer.Values(std::span(plan.guardedSrtSlots)); + writer.Value(plan.srtGuardOffset); + writer.Values(std::span(plan.cleanFlatSlots)); + writer.Values(std::span(plan.pureFlatSlots)); + writer.Value(plan.requiresSpecializationMemory); + writer.Value(plan.srtPlanComplete); + writer.Value(plan.resourceTrackingComplete); + encodeShaderInfo(writer, plan.info); + const auto& fill = plan.uniformFill.fill; + writer.Value(fill.kind); + writer.Value(fill.resource); + for (const auto stride : fill.groupStride) writer.Value(stride); + writer.Value(fill.words); + writer.Value(fill.value); + for (const auto* value : plan.uniformFill.values) writer.Value(valueIndex(value)); + return owned; +} + +struct PlanValueRecord { + IrOpcode opcode = IrOpcode::Identity; + IrType type = IrType::Void; + std::uint32_t id = 0; + std::uint64_t flags = 0; + bool hasImmediate = false; + std::uint64_t immediate = 0; + GuestRegister registerValue{}; + std::vector arguments; + std::vector phiBlocks; +}; + +bool decodePlan(Reader& reader, IrResourcePlan& plan) { + reader.Value(plan.stage); + reader.Value(plan.shaderHash); + reader.Value(plan.userDataBase); + reader.Value(plan.userDataCount); + reader.Value(plan.srgbDecodeFormats); + std::vector blockIds; + reader.Values(blockIds); + std::vector records; + std::uint32_t current = 0; + reader.List(records, 6, [&](Reader& in, PlanValueRecord& record) { + constexpr std::uint64_t Word = std::numeric_limits::max(); + const auto opcode = in.Compact(); + const auto type = in.Compact(); + const auto id = in.Compact(); + record.flags = in.Compact(); + const auto registerValue = in.Compact(); + const auto shape = in.Compact(); + const auto arguments = shape >> 1u; + if (opcode >= static_cast(IrOpcode::Count) || type > Word || id > Word || (registerValue >> 4u) > Word || arguments > in.Remaining()) { + in.Fail(); + return; + } + record.opcode = static_cast(opcode); + record.type = static_cast(type); + record.id = static_cast(id); + record.registerValue = {static_cast(registerValue & 0xfu), static_cast(registerValue >> 4u)}; + record.hasImmediate = (shape & 1u) != 0u; + if (record.hasImmediate) record.immediate = in.Compact(); + record.arguments.resize(static_cast(arguments)); + for (auto& argument : record.arguments) { + const auto code = in.Compact(); + if (code == 0u) { + argument = NoPlanIndex; + continue; + } + const auto zigzag = code - 1u; + const auto delta = static_cast(zigzag >> 1u) ^ -static_cast(zigzag & 1u); + const auto index = static_cast(current) + delta; + if (index < 0 || index >= static_cast(records.size())) { + in.Fail(); + return; + } + argument = static_cast(index); + } + if (record.opcode == IrOpcode::Phi) { + record.phiBlocks.resize(record.arguments.size()); + for (auto& block : record.phiBlocks) { + const auto index = in.Compact(); + block = index < blockIds.size() ? static_cast(index) : NoPlanIndex; + if (block == NoPlanIndex) in.Fail(); + } + } + ++current; + }); + if (!reader.Ok()) return false; + plan.blockStorage.reserve(blockIds.size()); + for (const auto id : blockIds) plan.blockStorage.push_back(std::make_unique(id)); + plan.valueStorage.reserve(records.size()); + for (const auto& record : records) { + auto value = std::make_unique(record.opcode, record.type, record.id); + value->SetFlags(record.flags); + if (record.hasImmediate) value->SetImmediateU64(record.immediate); + value->SetRegister(record.registerValue); + plan.valueStorage.push_back(std::move(value)); + } + bool valid = true; + const auto valueAt = [&](std::uint32_t index) -> IrValue* { + if (index == NoPlanIndex) return nullptr; + if (index >= plan.valueStorage.size()) { + valid = false; + return nullptr; + } + return plan.valueStorage[index].get(); + }; + for (std::size_t index = 0; index < records.size(); ++index) { + const auto& record = records[index]; + auto& value = *plan.valueStorage[index]; + for (std::size_t operand = 0; operand < record.arguments.size(); ++operand) { + auto* argument = valueAt(record.arguments[operand]); + if (value.IsPhi()) { + value.AddPhiOperand(plan.blockStorage[record.phiBlocks[operand]].get(), argument); + } else { + value.AddArgument(argument); + } + } + } + reader.List(plan.memoryInfo, 78, decodeMemoryInfo); + reader.List(plan.descriptorSources, 62, [&](Reader& in, DescriptorSource& source) { + for (auto& dword : source.dwords) dword = valueAt(in.Get()); + in.Value(source.dwordCount); + const bool indirect = in.Get(); + DescriptorSource::IndirectImage table; + in.Value(table.materialSource); + in.Value(table.heapSource); + in.Value(table.selectorStride); + in.Value(table.selectorOffset); + in.Value(table.keyArg); + in.Value(table.entryOffset); + in.Value(table.hasMaterial); + source.indirectImage = indirect ? std::optional(table) : std::nullopt; + }); + reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { + block.condition = valueAt(in.Get()); + in.Values(block.successors); + in.Values(block.sources); + in.Values(block.images); + }); + reader.Values(plan.materializationSources); + reader.List(plan.srtReads, 8, [&](Reader& in, SrtRead& read) { + read.value = valueAt(in.Get()); + in.Value(read.flatOffset); + }); + reader.Values(plan.guardedSrtSlots); + reader.Value(plan.srtGuardOffset); + reader.Values(plan.cleanFlatSlots); + reader.Values(plan.pureFlatSlots); + reader.Value(plan.requiresSpecializationMemory); + reader.Value(plan.srtPlanComplete); + reader.Value(plan.resourceTrackingComplete); + decodeShaderInfo(reader, plan.info); + auto& fill = plan.uniformFill.fill; + reader.Value(fill.kind); + reader.Value(fill.resource); + for (auto& stride : fill.groupStride) reader.Value(stride); + reader.Value(fill.words); + reader.Value(fill.value); + for (auto& value : plan.uniformFill.values) value = valueAt(reader.Get()); + if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; + const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; + if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; + if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.indirectImage.has_value() || (knownSource(source.indirectImage->heapSource) && knownSource(source.indirectImage->materialSource))); })) return false; + if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; + if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; + if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; + return std::ranges::all_of(plan.controlFlow, [&](const ResourceBlock& block) { + 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(); }); + }); +} + constexpr std::string_view NeutralSwitches[] = { "APS5_PROFILE_DRAW", "APS5_DUMP_IR", "APS5_NO_CODE_HASH_KEY", "APS5_NO_FAILURE_MEMO", "APS5_NO_PERF_FRONTEND_PAIR", + "APS5_NO_PERF_DISK_PLANS", "APS5_NO_PERF_DOM_INTERSECTION", "APS5_NO_PERF_ITERATION_SELECTION", + "APS5_NO_PERF_LAYOUT_PAIR", "APS5_NO_RESULT_MEMO", }; @@ -717,6 +1193,35 @@ std::string hex(std::uint64_t value) { return text; } +std::vector wrapEntry(std::span key, std::span payload) { + const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; + const auto payloadOffset = PayloadOffset(key.size()); + std::vector file(payloadOffset + payload.size()); + std::memcpy(file.data(), &header, sizeof(header)); + std::memcpy(file.data() + sizeof(header), key.data(), key.size()); + if (!payload.empty()) std::memcpy(file.data() + payloadOffset, payload.data(), payload.size()); + return file; +} + +LoadStatus unwrapEntry(std::span file, std::span key, std::span& payload) { + if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; + FileHeader header; + std::memcpy(&header, file.data(), sizeof(header)); + if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; + const auto body = file.size() - sizeof(header); + if (header.keyBytes > body) return LoadStatus::Rejected; + const auto payloadOffset = PayloadOffset(static_cast(header.keyBytes)); + if (payloadOffset > file.size() || header.payloadBytes != file.size() - payloadOffset) return LoadStatus::Rejected; + const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); + const auto padding = file.subspan(sizeof(header) + storedKey.size(), payloadOffset - sizeof(header) - storedKey.size()); + if (std::ranges::any_of(padding, [](std::byte value) { return value != std::byte{0}; })) return LoadStatus::Rejected; + payload = file.subspan(payloadOffset); + if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; + if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; + if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; + return LoadStatus::Loaded; +} + class DiskStore { public: DiskStore() { @@ -731,43 +1236,35 @@ public: [[nodiscard]] const std::filesystem::path& Directory() const { return directory; } bool Load(std::span key, CompiledVariant& variant) { - const auto started = std::chrono::steady_clock::now(); - const auto name = EntryName(key); - thread_local std::vector* fileSlot = nullptr; - auto& file = ThreadOwned(fileSlot); - bool loaded = false; - if (!ReadWholeFile(directory / name, file)) { - misses.fetch_add(1, std::memory_order_relaxed); - } else { - switch (DecodeEntry(file, key, variant)) { - case LoadStatus::Loaded: - hits.fetch_add(1, std::memory_order_relaxed); - bytesRead.fetch_add(file.size(), std::memory_order_relaxed); - loaded = true; - break; - case LoadStatus::Absent: - case LoadStatus::KeyMismatch: - misses.fetch_add(1, std::memory_order_relaxed); - break; - case LoadStatus::Rejected: { - const auto failures = loadFailures.fetch_add(1, std::memory_order_relaxed); - if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), file.size()); - break; - } - } - } - if (file.capacity() > (4u << 20u)) std::vector().swap(file); - loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); - report(false); - return loaded; + return load(key, [&](std::span file, std::shared_ptr owner) { return DecodeEntry(file, key, variant, std::move(owner)); }, hits, misses, loadFailures); + } + + bool LoadPlan(std::span key, IrResourcePlan& plan) { + return load(key, [&](std::span file, std::shared_ptr) { return DecodePlanEntry(file, key, plan); }, planHits, planMisses, planLoadFailures); } void Store(std::vector key, std::shared_ptr variant) { - { - std::lock_guard lock(mutex); - jobs.push_back({std::move(key), std::move(variant)}); - } - wake.notify_one(); + enqueue({std::move(key), std::move(variant), nullptr}); + } + + void StorePlan(std::vector key, std::shared_ptr plan) { + enqueue({std::move(key), nullptr, std::move(plan)}); + } + + std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { + const auto path = directory / EntryName(key); + const auto file = EncodeEntry(key, variant); + if (!WriteFileAtomically(path, file)) return nullptr; + writes.fetch_add(1, std::memory_order_relaxed); + bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); + auto mapped = MapFile(path); + if (mapped == nullptr) return nullptr; + auto persisted = std::make_shared(); + const auto bytes = mapped->Bytes(); + if (DecodeEntry(bytes, key, *persisted, std::move(mapped)) != LoadStatus::Loaded) return nullptr; + persisted->layout = variant.layout; + persisted->artifact.variantId = variant.artifact.variantId; + return persisted; } void Flush() { @@ -784,6 +1281,10 @@ public: counters.writeFailures = writeFailures.load(std::memory_order_relaxed); counters.bytesRead = bytesRead.load(std::memory_order_relaxed); counters.bytesWritten = bytesWritten.load(std::memory_order_relaxed); + counters.planHits = planHits.load(std::memory_order_relaxed); + counters.planMisses = planMisses.load(std::memory_order_relaxed); + counters.planWrites = planWrites.load(std::memory_order_relaxed); + counters.planLoadFailures = planLoadFailures.load(std::memory_order_relaxed); return counters; } @@ -791,8 +1292,58 @@ private: struct Job { std::vector key; std::shared_ptr variant; + std::shared_ptr plan; }; + template + bool load(std::span key, TDecode&& decode, std::atomic& hitCount, std::atomic& missCount, std::atomic& failureCount) { + const auto started = std::chrono::steady_clock::now(); + const auto name = EntryName(key); + thread_local std::vector* fileSlot = nullptr; + auto& file = ThreadOwned(fileSlot); + bool loaded = false; + std::span bytes; + std::shared_ptr owner; + if (auto mapped = MapFile(directory / name)) { + bytes = mapped->Bytes(); + owner = std::move(mapped); + } + const bool present = owner != nullptr || ReadWholeFile(directory / name, file); + if (owner == nullptr) bytes = file; + if (!present) { + missCount.fetch_add(1, std::memory_order_relaxed); + } else { + switch (decode(bytes, std::move(owner))) { + case LoadStatus::Loaded: + hitCount.fetch_add(1, std::memory_order_relaxed); + bytesRead.fetch_add(bytes.size(), std::memory_order_relaxed); + loaded = true; + break; + case LoadStatus::Absent: + case LoadStatus::KeyMismatch: + missCount.fetch_add(1, std::memory_order_relaxed); + break; + case LoadStatus::Rejected: { + const auto failures = failureCount.fetch_add(1, std::memory_order_relaxed); + if (failures < 8) std::fprintf(stderr, "[shader-disk-cache] rejected %s (%zu bytes): truncated, corrupt or another format; recompiling\n", name.c_str(), bytes.size()); + break; + } + } + } + if (file.capacity() > (4u << 20u)) std::vector().swap(file); + loadNanoseconds.fetch_add(static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed); + report(false); + return loaded; + } + + void enqueue(Job job) { + { + std::lock_guard lock(mutex); + jobs.push_back(std::move(job)); + } + wake.notify_one(); + } + void run() { std::fprintf(stderr, "[shader-disk-cache] %s (source version %s, format %u)\n", directory.string().c_str(), hex(SourceVersion()).c_str(), FormatVersion); housekeeping(); @@ -817,9 +1368,9 @@ private: } void write(const Job& job) { - const auto file = EncodeEntry(job.key, *job.variant); - if (WriteFileAtomically(directory / EntryName(job.key), file)) { - writes.fetch_add(1, std::memory_order_relaxed); + const auto file = job.plan != nullptr ? EncodePlanEntry(job.key, *job.plan) : EncodeEntry(job.key, *job.variant); + if (!file.empty() && WriteFileAtomically(directory / EntryName(job.key), file)) { + (job.plan != nullptr ? planWrites : writes).fetch_add(1, std::memory_order_relaxed); bytesWritten.fetch_add(file.size(), std::memory_order_relaxed); } else { const auto failures = writeFailures.fetch_add(1, std::memory_order_relaxed); @@ -869,11 +1420,13 @@ private: const auto totals = Totals(); const auto loadNs = loadNanoseconds.load(std::memory_order_relaxed); const auto delta = [](std::uint64_t after, std::uint64_t before) { return static_cast(after - before); }; - if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures) return; - std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes)\n", + if (totals.hits == reported.hits && totals.misses == reported.misses && totals.writes == reported.writes && totals.loadFailures == reported.loadFailures && totals.writeFailures == reported.writeFailures && totals.planHits == reported.planHits && totals.planMisses == reported.planMisses && totals.planWrites == reported.planWrites && totals.planLoadFailures == reported.planLoadFailures) return; + std::fprintf(stderr, "[shader-disk-cache] (10 s): %llu hits, %llu misses, %llu writes (%.1f MiB), %llu load failures, %llu write failures; resource plans: %llu hits, %llu misses, %llu writes, %llu load failures; %.1f MiB read in %.1f ms (totals: %llu hits, %llu misses, %llu writes; plans %llu hits, %llu misses)\n", delta(totals.hits, reported.hits), delta(totals.misses, reported.misses), delta(totals.writes, reported.writes), static_cast(totals.bytesWritten - reported.bytesWritten) / (1024.0 * 1024.0), - delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, - static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes)); + delta(totals.loadFailures, reported.loadFailures), delta(totals.writeFailures, reported.writeFailures), + delta(totals.planHits, reported.planHits), delta(totals.planMisses, reported.planMisses), delta(totals.planWrites, reported.planWrites), delta(totals.planLoadFailures, reported.planLoadFailures), + static_cast(totals.bytesRead - reported.bytesRead) / (1024.0 * 1024.0), static_cast(loadNs - reportedLoadNs) / 1e6, + static_cast(totals.hits), static_cast(totals.misses), static_cast(totals.writes), static_cast(totals.planHits), static_cast(totals.planMisses)); reported = totals; reportedLoadNs = loadNs; } @@ -886,6 +1439,7 @@ private: std::deque jobs; bool writing = false; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; + std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; std::atomic lastReport{0}; std::mutex reportMutex; Counters reported; @@ -923,6 +1477,23 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s key.insert(key.end(), switches.begin(), switches.end()); } +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key) { + key.clear(); + Writer writer(key); + writer.Value(FileMagic); + writer.Value(FormatVersion); + writer.Value(SourceVersion()); + writer.Value(PlanEntryKind); + thread_local std::vector* memoryKeySlot = nullptr; + auto& memoryKey = ThreadOwned(memoryKeySlot); + RecompileCacheKey::Build(request, memoryKey); + writer.Values(std::span(memoryKey)); + writer.Values(request.shader.code); + writer.Value(hostSubgroupSize); + const auto& switches = switchKey(); + key.insert(key.end(), switches.begin(), switches.end()); +} + std::string EntryName(std::span key) { return hex(HashBytes(key, 0x5eed0001ull)) + hex(HashBytes(key, 0x5eed0002ull)) + ".bin"; } @@ -950,27 +1521,14 @@ std::vector EncodeEntry(std::span key, const Compile encodeArtifact(writer, variant.artifact); encodeInfo(writer, variant.info); encodeAllocation(writer, variant.bindings); - const FileHeader header{FileMagic, FormatVersion, SourceVersion(), key.size(), payload.size(), HashBytes(key), HashBytes(payload)}; - std::vector file(sizeof(header) + key.size() + payload.size()); - std::memcpy(file.data(), &header, sizeof(header)); - std::memcpy(file.data() + sizeof(header), key.data(), key.size()); - std::memcpy(file.data() + sizeof(header) + key.size(), payload.data(), payload.size()); - return file; + return wrapEntry(key, payload); } -LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant) { - if (file.size() < sizeof(FileHeader)) return LoadStatus::Rejected; - FileHeader header; - std::memcpy(&header, file.data(), sizeof(header)); - if (header.magic != FileMagic || header.format != FormatVersion || header.sourceVersion != SourceVersion()) return LoadStatus::Rejected; - const auto body = file.size() - sizeof(header); - if (header.keyBytes > body || header.payloadBytes != body - header.keyBytes) return LoadStatus::Rejected; - const auto storedKey = file.subspan(sizeof(header), static_cast(header.keyBytes)); - const auto payload = file.subspan(sizeof(header) + static_cast(header.keyBytes)); - if (HashBytes(storedKey) != header.keyHash) return LoadStatus::Rejected; - if (storedKey.size() != key.size() || !std::equal(storedKey.begin(), storedKey.end(), key.begin())) return LoadStatus::KeyMismatch; - if (HashBytes(payload) != header.payloadHash) return LoadStatus::Rejected; - Reader reader(payload); +LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner) { + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; + Reader reader(payload, std::move(owner)); CompiledVariant decoded; decodeArtifact(reader, decoded.artifact); decodeInfo(reader, decoded.info); @@ -982,6 +1540,32 @@ LoadStatus DecodeEntry(std::span file, std::span& out) { + Writer writer(out); + return encodePlan(writer, plan); +} + +bool DecodePlan(std::span bytes, IrResourcePlan& plan) { + Reader reader(bytes); + IrResourcePlan decoded; + if (!decodePlan(reader, decoded)) return false; + plan = std::move(decoded); + return true; +} + +std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan) { + std::vector payload; + if (!EncodePlan(plan, payload)) return {}; + return wrapEntry(key, payload); +} + +LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan) { + std::span payload; + const auto status = unwrapEntry(file, key, payload); + if (status != LoadStatus::Loaded) return status; + return DecodePlan(payload, plan) ? LoadStatus::Loaded : LoadStatus::Rejected; +} + bool Enabled() { return store().Enabled(); } @@ -1000,6 +1584,21 @@ void Store(std::vector key, std::shared_ptr va if (instance.Enabled()) instance.Store(std::move(key), std::move(variant)); } +bool LoadPlan(std::span key, IrResourcePlan& plan) { + auto& instance = store(); + return instance.Enabled() && instance.LoadPlan(key, plan); +} + +void StorePlan(std::vector key, std::shared_ptr plan) { + auto& instance = store(); + if (instance.Enabled() && plan != nullptr) instance.StorePlan(std::move(key), std::move(plan)); +} + +std::shared_ptr Persist(std::span key, const CompiledVariant& variant) { + auto& instance = store(); + return instance.Enabled() ? instance.Persist(key, variant) : nullptr; +} + void Flush() { auto& instance = store(); if (instance.Enabled()) instance.Flush(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 81bbd87beb..327eb2f30a 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILER_SHADERDISKCACHE_HPP #include "CompiledVariant.hpp" +#include "IntermediateRepresentation/IrMetadata/ResourcePlan.hpp" #include #include #include @@ -12,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 29; +inline constexpr std::uint32_t FormatVersion = 31; enum class LoadStatus { Loaded, @@ -29,6 +30,10 @@ struct Counters { std::uint64_t writeFailures = 0; std::uint64_t bytesRead = 0; std::uint64_t bytesWritten = 0; + std::uint64_t planHits = 0; + std::uint64_t planMisses = 0; + std::uint64_t planWrites = 0; + std::uint64_t planLoadFailures = 0; }; [[nodiscard]] std::uint64_t SourceVersion(); @@ -41,13 +46,22 @@ void EncodeResult(const RecompileResult& result, std::vector& out); [[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result); [[nodiscard]] std::vector EncodeEntry(std::span key, const CompiledVariant& variant); -[[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant); +[[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner = nullptr); + +void BuildPlanKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, std::vector& key); +[[nodiscard]] bool EncodePlan(const IrResourcePlan& plan, std::vector& out); +[[nodiscard]] bool DecodePlan(std::span bytes, IrResourcePlan& plan); +[[nodiscard]] std::vector EncodePlanEntry(std::span key, const IrResourcePlan& plan); +[[nodiscard]] LoadStatus DecodePlanEntry(std::span file, std::span key, IrResourcePlan& plan); [[nodiscard]] bool Enabled(); [[nodiscard]] std::filesystem::path EntryDirectory(); [[nodiscard]] bool Load(std::span key, CompiledVariant& variant); void Store(std::vector key, std::shared_ptr variant); +[[nodiscard]] std::shared_ptr Persist(std::span key, const CompiledVariant& variant); +[[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); +void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); [[nodiscard]] Counters Totals(); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 0b308f556c..47ffeb796c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -17,6 +17,7 @@ void DefineBdaFaultFunction(SpirvEmitterState& state); void DefineBdaDwordReadFunctions(SpirvEmitterState& state); void DefineBdaSpanReadFunctions(SpirvEmitterState& state); void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason); void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target); @@ -34,7 +35,7 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index bbc32567d2..64699e33b9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index f5d6a95d9e..afd26653ac 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -128,6 +128,7 @@ struct SpirvEmitterState { std::uint32_t dot2F32F16Function = 0; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; + std::unordered_map> bdaGroupedLoads; std::uint32_t bdaStopValue = 0; std::uint32_t bdaPcOverride = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index c89d6a2112..03bd0330d6 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -18,6 +18,7 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 69ff59f2f7..757a979fa5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -300,6 +300,18 @@ bool AllLanesBit(const IrValue* bit) { return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } +bool HelperTest(const IrValue* predicate) { + return Is(predicate, IrOpcode::IEqual32) && EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); +} + +bool HelperOrFalseConjunct(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit->Argument(0)) || HelperOrFalseConjunct(bit->Argument(1)); + return HelperTest(bit); +} + bool EveryInvocationEnters(IrShaderStage stage) { return stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation; } @@ -312,9 +324,10 @@ public: bit = Resolved(bit); if (bit == nullptr || !Spend()) return false; if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); - if (Is(bit, IrOpcode::LogicalAnd)) return ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); + if (Is(bit, IrOpcode::LogicalAnd)) return HelperOrFalseConjunct(bit) || ZeroBit(bit->Argument(0)) || ZeroBit(bit->Argument(1)); if (Is(bit, IrOpcode::SelectU1)) return ZeroBit(bit->Argument(1)) && ZeroBit(bit->Argument(2)); if (Is(bit, IrOpcode::Phi)) return Incoming(bit, [this](const IrValue* incoming) { return ZeroBit(incoming); }); + if (Is(bit, IrOpcode::IEqual32)) return HelperTest(bit); const auto words = LaneBitWords(bit); return words && Word((*words)[0], 0u) && Word((*words)[1], 1u); } @@ -332,7 +345,7 @@ private: if (predicate->HasImmediate()) return everyInvocationEnters && predicate->Type() == IrType::Bool && predicate->ImmediateBool(); switch (predicate->Opcode()) { case IrOpcode::LogicalAnd: return NotHelper(predicate->Argument(0)) || NotHelper(predicate->Argument(1)); - case IrOpcode::IEqual32: return EitherOrder(predicate, [](const IrValue* builtin, const IrValue* zero) { return IsU32(zero, 0u) && Is(builtin, IrOpcode::GetBuiltin) && IsU32(builtin->Argument(0), static_cast(StageInputKind::HelperInvocation)) && IsU32(builtin->Argument(1), 0u); }); + case IrOpcode::IEqual32: return HelperTest(predicate); default: return ZeroBit(predicate); } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 4742b649a6..dd83663928 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -224,11 +224,17 @@ std::uint32_t FunctionLdsDwords(const IrProgram& program) { std::uint64_t needed = 0; for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { - if (SharedAccessOf(inst->Opcode()) == SharedAccess::None) continue; + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None) continue; const auto index = inst->Flags().index; if (index >= program.Resources().memoryInfo.size()) return FunctionLdsDwordLimit; const auto& memory = program.Resources().memoryInfo[index]; if (memory.kind != ResourceKind::Lds) continue; + if (access == SharedAccess::Append || access == SharedAccess::Consume) { + needed = std::max(needed, (std::uint64_t{memory.offset} >> 2u) + 1u); + if (needed >= FunctionLdsDwordLimit) return FunctionLdsDwordLimit; + continue; + } const auto dwords = SharedAccessDwords(inst->Opcode()); if (dwords == 0u || inst->ArgumentCount() == 0) return FunctionLdsDwordLimit; const auto address = bound.Of(inst->Argument(0)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index ca8c1a2678..4b0d60d921 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -75,6 +75,11 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast(reason)), address, bytes, instruction); } +void RecordBdaFaultReason(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t reason) { + if (state.bdaFaultFunction == 0 || !state.program.Info().usesDma) throw std::runtime_error("BDA fault function is missing"); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, reason, address, bytes, instruction); +} + void RecordBdaFaultWords(SpirvEmitterState& state, std::uint32_t addressLow, std::uint32_t addressHigh, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { if (state.program.Info().usesDma) { const auto u64 = TypeScalarU64(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index 248ff205c0..b48e8775b5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -2,6 +2,9 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include +#include +#include +#include namespace ShaderRecompiler { @@ -24,11 +27,6 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto boolean = TypeBool(state); const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; - const auto load = [&](std::uint32_t pointer) { - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, u32, value, pointer); - return value; - }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); @@ -39,11 +37,6 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); EmitLabel(state, state.module.AllocateId()); - const auto low = state.module.AllocateId(); - const auto high = state.module.AllocateId(); - const auto pointer = TypePointer(state, spv::StorageClassFunction, u32); - state.module.AddFunction(spv::OpVariable, pointer, low, spv::StorageClassFunction); - state.module.AddFunction(spv::OpVariable, pointer, high, spv::StorageClassFunction); const auto fail = [&](std::uint32_t condition, BdaAbi::FaultReason reason) { if (recordFaults) { ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); @@ -51,29 +44,53 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r ReturnBdaZeroIf(state, condition); } }; + const auto failFirst = [&](std::initializer_list> checks) { + auto reason = constant(0u); + for (auto check = std::rbegin(checks); check != std::rend(checks); ++check) { + reason = Select(state, u32, check->first, constant(static_cast(check->second)), reason); + } + const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); + if (!recordFaults) { + ReturnBdaZeroIf(state, failed); + return; + } + const auto failedLabel = state.module.AllocateId(); + const auto next = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, failed, failedLabel, next); + EmitLabel(state, failedLabel); + RecordBdaFaultReason(state, address, bytes, instruction, reason); + state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); + EmitLabel(state, next); + }; const auto length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, u32, length, state.bdaPagetableVariable, 0u); fail(binary(spv::OpULessThan, boolean, length, constant(4)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable); const auto count = BdaLoadWord(state, constant(1)); const auto available = binary(spv::OpISub, u32, length, constant(4)); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow); const auto end = binary(spv::OpIAdd, u64, address, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow); - state.module.AddFunction(spv::OpStore, low, constant(0)); - state.module.AddFunction(spv::OpStore, high, count); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(0)), constant(BdaAbi::Version)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(2)), constant(sizeof(BdaAbi::Range))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, constant(3)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, available, constant(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpINotEqual, boolean, count, binary(spv::OpShiftRightLogical, u32, available, constant(3))), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpIEqual, boolean, bytes, constant(0)), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, end, address), BdaAbi::FaultReason::Overflow}, + }); const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); const auto continuation = state.module.AllocateId(); const auto merge = state.module.AllocateId(); + const auto entryLabel = state.currentLabel; state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); - const auto lower = load(low); - const auto upper = load(high); + const auto lower = state.module.AllocateId(); + const auto upper = state.module.AllocateId(); + const auto newLow = state.module.AllocateId(); + const auto newHigh = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, lower, constant(0), entryLabel, newLow, continuation); + state.module.AddFunction(spv::OpPhi, u32, upper, count, entryLabel, newHigh, continuation); const auto search = binary(spv::OpULessThan, boolean, lower, upper); state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, search, body, merge); @@ -81,17 +98,13 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto midpoint = binary(spv::OpIAdd, u32, lower, binary(spv::OpShiftRightLogical, u32, binary(spv::OpISub, u32, upper, lower), constant(1))); const auto entry = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, midpoint, constant(8))); const auto before = binary(spv::OpULessThan, boolean, address, BdaLoadAddress(state, entry)); - const auto newLow = state.module.AllocateId(); - const auto newHigh = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, u32, newLow, before, lower, binary(spv::OpIAdd, u32, midpoint, constant(1))); state.module.AddFunction(spv::OpSelect, u32, newHigh, before, midpoint, upper); - state.module.AddFunction(spv::OpStore, low, newLow); - state.module.AddFunction(spv::OpStore, high, newHigh); state.module.AddFunction(spv::OpBranch, continuation); EmitLabel(state, continuation); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - const auto index = load(low); + const auto index = lower; fail(binary(spv::OpIEqual, boolean, index, constant(0)), BdaAbi::FaultReason::Unmapped); const auto selected = binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, binary(spv::OpISub, u32, index, constant(1)), constant(8))); const auto at = [&](std::uint32_t offset) { return binary(spv::OpIAdd, u32, selected, constant(offset)); }; @@ -99,16 +112,18 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto finish = BdaLoadAddress(state, at(2)); const auto base = BdaLoadAddress(state, at(4)); const auto permissions = BdaLoadWord(state, at(6)); - fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable); - fail(binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped); - fail(binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission); - fail(binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable); const auto offset = binary(spv::OpISub, u64, address, begin); const auto result = binary(spv::OpIAdd, u64, base, offset); - fail(binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow); const auto deviceEnd = binary(spv::OpIAdd, u64, result, Unary(state, spv::OpUConvert, u64, bytes)); - fail(binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow); + failFirst({ + {binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped}, + {binary(spv::OpINotEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(permission)), BdaAbi::FaultReason::Permission}, + {binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable}, + {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, + {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, + }); state.module.AddFunction(spv::OpReturnValue, result); state.module.AddFunction(spv::OpFunctionEnd); return function; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 663a001ba7..b43f89f997 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -66,16 +66,41 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui return value; } -std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { - auto result = ConstantU32(state, 0u); - for (std::uint32_t byte = 0; byte < bytes; ++byte) { - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto value = EmitBdaByte(state, guest, instruction, accessMask); - result = Binary(state, spv::OpBitwiseOr, TypeU32(state), result, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), value, ConstantU32(state, byte * 8u))); - } +std::uint32_t EmitBdaByteLoop(SpirvEmitterState& state, std::uint32_t address, std::uint32_t count, std::uint32_t instruction, std::uint32_t accessMask) { + const auto u32 = TypeU32(state); + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto byte = state.module.AllocateId(); + const auto result = state.module.AllocateId(); + const auto nextByte = state.module.AllocateId(); + const auto nextResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, byte, ConstantU32(state, 0u), entry, nextByte, continuation); + state.module.AddFunction(spv::OpPhi, u32, result, ConstantU32(state, 0u), entry, nextResult, continuation); + const auto more = Binary(state, spv::OpULessThan, TypeBool(state), byte, count); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + const auto value = EmitBdaByte(state, guest, instruction, accessMask); + state.module.AddFunction(spv::OpBitwiseOr, u32, nextResult, result, Binary(state, spv::OpShiftLeftLogical, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, byte, ConstantU32(state, 3u)))); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextByte, byte, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); return result; } +std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { + if (bytes == 1u) return EmitBdaByte(state, address, instruction, accessMask); + return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); +} + // The dwords of a wide load that the program extracts (all of them when the composite is used // whole). Dead-code elimination has dropped the unused extracts, as it dropped unused dword loads // when every dword was its own instruction. @@ -204,26 +229,48 @@ std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent, state.module.AddFunction(spv::OpFunctionCall, values, loaded, loader, physical, firstBytes, used); return loaded; }, [&] { - std::array bytes{}; - for (std::uint32_t dword = 0; dword < 4u; ++dword) { - bytes[dword] = EmitValueOrZeroIfCondition(state, isUsed(dword), [&] { - const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, ConstantU32(state, dword)); - const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { - return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, ConstantU32(state, dword * 4u)), instruction); - }); - const auto pair = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); - if (stops) { - const auto stopped = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); - StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); - } - const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, value, pair, 0u); - return value; + const auto entry = state.currentLabel; + const auto header = state.module.AllocateId(); + const auto body = state.module.AllocateId(); + const auto continuation = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto dword = state.module.AllocateId(); + const auto gathered = state.module.AllocateId(); + const auto nextDword = state.module.AllocateId(); + const auto nextGathered = state.module.AllocateId(); + const auto zeros = state.module.Constant(spv::OpConstantComposite, values, zero, zero, zero, zero); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, u32, dword, zero, entry, nextDword, continuation); + state.module.AddFunction(spv::OpPhi, values, gathered, zeros, entry, nextGathered, continuation); + const auto more = Binary(state, spv::OpULessThan, boolean, dword, ConstantU32(state, 4u)); + state.module.AddFunction(spv::OpLoopMerge, merge, continuation, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, more, body, merge); + EmitLabel(state, body); + const auto used32 = Binary(state, spv::OpINotEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, used, Binary(state, spv::OpShiftLeftLogical, u32, ConstantU32(state, 1u), dword)), zero); + const auto value = EmitValueOrZeroIfCondition(state, used32, [&] { + const auto isFirst = Binary(state, spv::OpIEqual, boolean, first, dword); + const auto dwordAddress = EmitValueIfElse(state, isFirst, u64, [&] { return firstAddress; }, [&] { + return AddBdaSignedOffset(state, address, Binary(state, spv::OpIAdd, u32, offset, Binary(state, spv::OpShiftLeftLogical, u32, dword, ConstantU32(state, 2u))), instruction); }); - } - return composite(bytes); + const auto pair = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), pair, reader, dwordAddress, instruction); + if (stops) { + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, stopped, pair, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, boolean, stopped, zero)); + } + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, loaded, pair, 0u); + return loaded; + }); + state.module.AddFunction(spv::OpVectorInsertDynamic, values, nextGathered, gathered, value, dword); + state.module.AddFunction(spv::OpBranch, continuation); + EmitLabel(state, continuation); + state.module.AddFunction(spv::OpIAdd, u32, nextDword, dword, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpBranch, header); + EmitLabel(state, merge); + return gathered; }); auto returned = read; if (stops) { @@ -359,7 +406,6 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe for (const auto extension : {"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}) { if (std::find(target.supportedExtensions.begin(), target.supportedExtensions.end(), extension) == target.supportedExtensions.end()) throw std::runtime_error(std::string("BDA requires unsupported extension ") + extension); } - if (program.Resources().stage == IrShaderStage::TessellationControl) throw std::runtime_error("BDA fault termination requires a barrier-safe tessellation-control execution protocol"); } bool BdaInvocationsMayStop(const IrProgram& program) { @@ -587,12 +633,12 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std // unaligned mirror or a wrapping span falls back to the byte path. That path repeats, per extracted // dword in order, the address, overflow check and byte lookups of a separate dword load, so every // fault is recorded exactly as one; the probe records none, and a span it accepts cannot fault. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword) { +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword, std::uint32_t usedDwords) { auto& state = ctx.state; if (dwords == 0u || dwords > 4u) ctx.Fail(inst, "unsupported BDA read width"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); const auto instruction = BdaInstructionPc(state, inst); - const auto used = everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); + const auto used = usedDwords != 0u ? usedDwords : everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); const auto isUsed = [&](std::uint32_t dword) { return (used & (1u << dword)) != 0u; }; std::array values{}; std::uint32_t first = dwords; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 93902d26d5..b56eb7a847 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -243,6 +244,24 @@ std::uint32_t LodU32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) return AddressU32(ctx, access, component); } +bool VolumeSliceAddress(const ImageEmitAccess& access) { + return access.mem.imageDimension == RdnaImageDimension::Dim2DArray && access.image.dimension == RdnaImageDimension::Dim3D; +} + +std::uint32_t VolumeSliceInRange(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t image, std::optional lod) { + auto& state = ctx.state; + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto size = state.module.AllocateId(); + if (lod.has_value()) { + state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 3), size, image, *lod); + } else { + state.module.AddFunction(spv::OpImageQuerySize, TypeU32Vector(state, 3), size, image); + } + const auto depth = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), depth, size, 2u); + return Binary(state, spv::OpULessThan, TypeBool(state), AddressU32(ctx, access, 2u), depth); +} + std::uint32_t DrefValueF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const ImageSampleLayout& layout) { if (layout.dref == NoImageComponent || access.mem.imageAddressComponents <= layout.dref) { ctx.Fail(access.inst, "has no depth reference in the image address"); @@ -964,6 +983,9 @@ void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { if (access.mem.imageDimension == RdnaImageDimension::Dim3D && access.image.dimension == RdnaImageDimension::Dim2D) { condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, Binary(state, spv::OpIEqual, TypeBool(state), AddressU32(ctx, access, 2u), ConstantU32(state, 0u))); } + if (VolumeSliceAddress(access)) { + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, LoadSampledImageDescriptor(state, access.mem.resource, access.slot), LodU32(ctx, access))); + } ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { const auto descriptor = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); const auto color = state.module.AllocateId(); @@ -997,7 +1019,14 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Fail(access.inst, "stores through a bindless image table are unsupported"); } const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; - EmitIfCondition(state, ctx.Arg(access.inst, 3), [&]() { + auto condition = ctx.Arg(access.inst, 3); + if (VolumeSliceAddress(access)) { + if (access.image.mipMode == ImageMipMode::DynamicStorage) ctx.Fail(access.inst, "stores a 2D-array address to a 3D image with dynamic mips, which is not implemented"); + const auto image = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, ImageType(state, access.image), image, StorageImageDescriptorPointer(state, access.mem.resource)); + condition = Binary(state, spv::OpLogicalAnd, TypeBool(state), condition, VolumeSliceInRange(ctx, access, image, std::nullopt)); + } + 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); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index b0c4e7b325..44745b1c8b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -168,13 +168,18 @@ std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryRe return inBounds; } +bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) { + if (kind == ResourceKind::Scratch) return true; + return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr; +} + std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); } if (access.kind == ResourceKind::Lds || access.kind == ResourceKind::Scratch) { const auto pointer = state.module.AllocateId(); - const std::uint32_t storageClass = access.kind == ResourceKind::Scratch ? spv::StorageClassFunction : ShaderWorkgroupInput(state) != nullptr ? spv::StorageClassWorkgroup : spv::StorageClassFunction; + const std::uint32_t storageClass = InFunctionStorage(state, access.kind) ? spv::StorageClassFunction : spv::StorageClassWorkgroup; state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, storageClass), pointer, access.objectPointer, index); return pointer; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 64ae0cd782..c454eee2c5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -3,6 +3,7 @@ #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -202,7 +203,86 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +bool SameAddressValue(const IrValue* left, const IrValue* right, std::uint32_t depth = 0u) { + if (left == right) return true; + if (left == nullptr || right == nullptr || depth > 3u || left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; + if (left->HasImmediate() || right->HasImmediate()) return left->HasImmediate() && right->HasImmediate() && left->ArgumentCount() == 0u && left->ImmediateU32() == right->ImmediateU32(); + if (left->Opcode() != IrOpcode::GetAddressResource && left->Opcode() != IrOpcode::SelectU32) return false; + for (std::size_t index = 0; index < left->ArgumentCount(); ++index) { + const IrValue* first = left->Argument(index); + const IrValue* second = right->Argument(index); + if (first == nullptr || second == nullptr || !SameAddressValue(first->Resolve(), second->Resolve(), depth + 1u)) return false; + } + return true; +} + +bool WritesMemory(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + const auto shared = SharedAccessOf(opcode); + const auto address = AddressOpcodeInfoOf(opcode).access; + const auto image = ImageOpcodeInfoOf(opcode).access; + return buffer == BufferAccess::Write || buffer == BufferAccess::Atomic || (shared != SharedAccess::None && shared != SharedAccess::Read) || address == AddressAccess::Write || address == AddressAccess::Atomic || + image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; +} + +std::vector> AdjacentBdaLoads(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { + std::vector> members; + const IrBlock* block = inst.Parent(); + if (block == nullptr || ctx.state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; + const auto& instructions = block->Instructions(); + auto next = std::find(instructions.begin(), instructions.end(), &inst); + if (next == instructions.end()) return members; + std::uint32_t taken = 1u; + for (++next; next != instructions.end(); ++next) { + const IrValue* other = *next; + if (other->Opcode() != inst.Opcode()) { + if (WritesMemory(other->Opcode())) break; + continue; + } + const auto& otherMemory = ctx.Memory(*other); + if (otherMemory.planningOnly || otherMemory.kind != mem.kind || otherMemory.addressIsFull != mem.addressIsFull || otherMemory.coherent != mem.coherent || other->ArgumentCount() != inst.ArgumentCount()) continue; + bool sameArguments = true; + for (std::size_t index = 0; index < inst.ArgumentCount(); ++index) { + const IrValue* left = inst.Argument(index); + const IrValue* right = other->Argument(index); + if (left == nullptr || right == nullptr || !SameAddressValue(left->Resolve(), right->Resolve())) sameArguments = false; + } + const auto delta = otherMemory.offset - mem.offset; + if (!sameArguments || delta == 0u || delta > 12u || (delta & 3u) != 0u || (taken & (1u << (delta / 4u))) != 0u) continue; + taken |= 1u << (delta / 4u); + members.emplace_back(other, delta / 4u); + } + return members; +} + std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + auto& state = ctx.state; + if (bits == 32u) { + if (const auto grouped = state.bdaGroupedLoads.find(&inst); grouped != state.bdaGroupedLoads.end()) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); + return value; + } + const auto members = AdjacentBdaLoads(ctx, inst, mem); + if (!members.empty()) { + std::uint32_t used = 1u; + for (const auto& member : members) used |= 1u << member.second; + const auto vector = TypeU32Vector(state, 4); + const auto zero = ConstantU32(state, 0u); + const auto none = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); + const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { + const auto values = EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, 4u, false, used); + const auto read = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); + return read; + }); + for (const auto& member : members) state.bdaGroupedLoads[member.first] = {all, member.second}; + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, all, 0u); + return value; + } + } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); }); @@ -231,7 +311,7 @@ void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, con const auto merge = [&](std::uint32_t old) { return Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), old, Unary(state, spv::OpNot, TypeU32(state), mask)), value); }; - if (mem.kind == ResourceKind::Scratch) { + if (InFunctionStorage(state, mem.kind)) { const auto old = state.module.AllocateId(); if (resource.memoryAccess != 0u) { state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer, resource.memoryAccess); @@ -336,9 +416,6 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -constexpr std::uint32_t SharedApertureHigh = 0x80000000u; -constexpr std::uint32_t PrivateApertureHigh = 0x70000000u; - bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; } @@ -642,6 +719,47 @@ std::uint32_t EmitAtomicOperation(SpirvValueEmitContext& ctx, const IrValue& ins return old; } +std::uint32_t AtomicNext32(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { + const auto u32 = TypeU32(state); + const auto winner = [&](spv::Op compare) { + return Select(state, u32, Binary(state, compare, TypeBool(state), old, value), old, value); + }; + switch (opcode) { + case spv::OpAtomicExchange: + return value; + case spv::OpAtomicCompareExchange: + return Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); + case spv::OpAtomicIAdd: + return Binary(state, spv::OpIAdd, u32, old, value); + case spv::OpAtomicISub: + return Binary(state, spv::OpISub, u32, old, value); + case spv::OpAtomicSMin: + return winner(spv::OpSLessThan); + case spv::OpAtomicUMin: + return winner(spv::OpULessThan); + case spv::OpAtomicSMax: + return winner(spv::OpSGreaterThan); + case spv::OpAtomicUMax: + return winner(spv::OpUGreaterThan); + case spv::OpAtomicAnd: + return Binary(state, spv::OpBitwiseAnd, u32, old, value); + case spv::OpAtomicOr: + return Binary(state, spv::OpBitwiseOr, u32, old, value); + case spv::OpAtomicXor: + return Binary(state, spv::OpBitwiseXor, u32, old, value); + default: + throw std::runtime_error("AtomicNext32: opcode has no 32-bit replacement"); + } +} + +template +std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t pointer, TReplacement&& replacement) { + const auto old = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, replacement(old)); + return old; +} + template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); @@ -667,9 +785,11 @@ template std::uint32_t EmitAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, TReplacement&& replacement) { const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { + const auto next = [&](std::uint32_t old) { return replacement(ctx.state, old, value); - }); + }; + if (InFunctionStorage(ctx.state, mem.kind)) return UpdateFunctionElement(ctx.state, pointer, next); + return AtomicUpdate(ctx.state, pointer, mem.kind, next); }); } @@ -738,6 +858,10 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me } if (mem.kind == ResourceKind::Buffer) NoteBufferAtomicSite(zeroSkip, zeroIdentity && !zeroSkip); const auto apply = [&](std::uint32_t pointer) { + if (InFunctionStorage(state, mem.kind)) { + const auto value = ctx.Arg(inst, inst.ArgumentCount() - 2u); + return UpdateFunctionElement(state, pointer, [&](std::uint32_t old) { return AtomicNext32(state, SpirvAtomicOpcode(inst.Opcode()), old, value, 0u); }); + } const auto operation = [&]() { const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { @@ -857,7 +981,9 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst auto& state = ctx.state; const auto& mem = SharedMemory(ctx, inst); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - const auto old = AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { return replacement(state, current); }); + const auto next = [&](std::uint32_t current) { return replacement(state, current); }; + 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)); return old; @@ -914,33 +1040,19 @@ std::uint32_t AddressAtomicOpcode(IrOpcode opcode) { } std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { - const auto u32 = TypeU32(state); - const auto winner = [&](spv::Op compare) { - return Select(state, u32, Binary(state, compare, TypeBool(state), old, value), old, value); - }; switch (opcode) { case IrOpcode::AddressAtomicSwap32: - return value; case IrOpcode::AddressAtomicCmpSwap32: - return Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), old, comparator), value, old); case IrOpcode::AddressAtomicIAdd32: - return Binary(state, spv::OpIAdd, u32, old, value); case IrOpcode::AddressAtomicISub32: - return Binary(state, spv::OpISub, u32, old, value); case IrOpcode::AddressAtomicSMin32: - return winner(spv::OpSLessThan); case IrOpcode::AddressAtomicUMin32: - return winner(spv::OpULessThan); case IrOpcode::AddressAtomicSMax32: - return winner(spv::OpSGreaterThan); case IrOpcode::AddressAtomicUMax32: - return winner(spv::OpUGreaterThan); case IrOpcode::AddressAtomicAnd32: - return Binary(state, spv::OpBitwiseAnd, u32, old, value); case IrOpcode::AddressAtomicOr32: - return Binary(state, spv::OpBitwiseOr, u32, old, value); case IrOpcode::AddressAtomicXor32: - return Binary(state, spv::OpBitwiseXor, u32, old, value); + return AtomicNext32(state, AddressAtomicOpcode(opcode), old, value, comparator); case IrOpcode::AddressAtomicInc32: return AtomicIncrement(state, old, value); case IrOpcode::AddressAtomicDec32: @@ -1176,6 +1288,9 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; 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); }); + } 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), count); return value; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index debd3304b6..395e365f6e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -288,7 +288,50 @@ void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { state.module.AddFunction(spv::OpStore, pointer, mask); } +void StoreDitheredAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + constexpr std::array masks{{ + 0x00000000u, 0x00000010u, 0x00000004u, 0x10000004u, 0x10000002u, 0x10100002u, 0x04100002u, 0x04101002u, + 0x04101001u, 0x04101011u, 0x04041011u, 0x14041011u, 0x12041011u, 0x12141011u, 0x12140411u, 0x12141411u, + 0x12141411u, 0x12141431u, 0x12141415u, 0x32141415u, 0x32121415u, 0x32321415u, 0x16321415u, 0x16323415u, + 0x15323415u, 0x15323435u, 0x15163435u, 0x35163435u, 0x35163235u, 0x35363235u, 0x35361635u, 0x35363635u, + 0x35363635u, 0x35363675u, 0x3536363du, 0x7536363du, 0x75363637u, 0x75763637u, 0x3d763637u, 0x3d767637u, + 0x3d757637u, 0x3d757677u, 0x3d3d7677u, 0x7d3d7677u, 0x773d7677u, 0x777d7677u, 0x777d3e77u, 0x777d7e77u, + 0x777d7e77u, 0x777d7ef7u, 0x777d7e7fu, 0xf77d7e7fu, 0xf7777e7fu, 0xf7f77e7fu, 0x7ff77e7fu, 0x7ff7fe7fu, + 0x7ff7fd7fu, 0x7ff7fdffu, 0x7f7ffdffu, 0xff7ffdffu, 0xff7ff7ffu, 0xfffff7ffu, 0xffff7fffu, 0xffffffffu, + }}; + const auto alpha = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); + const auto positive = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), alpha, ConstantF32Value(state, 0.0f)), alpha, ConstantF32Value(state, 0.0f)); + const auto bounded = Select(state, TypeF32(state), Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), positive, ConstantF32Value(state, 63.0f / 64.0f)), ConstantF32Value(state, 63.0f / 64.0f), positive); + const auto index = Unary(state, spv::OpConvertFToU, TypeU32(state), Binary(state, spv::OpFMul, TypeF32(state), bounded, ConstantF32Value(state, 64.0f))); + std::array words{}; + for (std::size_t i = 0; i < words.size(); ++i) words[i] = ConstantU32(state, masks[i]); + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); + const auto table = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, array), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, table, state.module.Constant(spv::OpConstantComposite, array, std::span(words))); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeU32(state)), pointer, table, index); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), packed, pointer); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeF32Vector(state, 4u), coord, InputVariableForKind(state, StageInputKind::FragCoord)); + std::array parity{}; + for (std::uint32_t i = 0; i < 2; ++i) { + const auto component = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), component, coord, i); + parity[i] = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpConvertFToU, TypeU32(state), component), ConstantU32(state, 1u)); + } + const auto pixel = Binary(state, spv::OpBitwiseOr, TypeU32(state), Binary(state, spv::OpShiftLeftLogical, TypeU32(state), parity[0], ConstantU32(state, 1u)), parity[1]); + const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), pixel, ConstantU32(state, 3u)); + const auto mask = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), packed, shift), ConstantU32(state, 255u)); + StoreCoverage(state, Unary(state, spv::OpBitcast, TypeI32(state), mask)); +} + void StoreAlphaCoverage(SpirvEmitterState& state, std::uint32_t color) { + if (state.inputInfo.pixel->psDitheredAlphaToCoverage) { + StoreDitheredAlphaCoverage(state, color); + return; + } const auto alpha = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), alpha, color, 3u); const auto below = state.module.AllocateId(); @@ -337,7 +380,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; const auto mode = MrtOutputMode(state, exp); - const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); + const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u || mode == 3u ? 0x3u : 0xfu); if (exp.compr) { for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -356,7 +399,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const } else { for (std::uint32_t component = 0; component < 4u; component++) { if (((exported >> component) & 1u) != 0u) { - raw.at(component) = ExportRawComponent(ctx, data, component); + raw.at(mode == 3u && component == 1u ? 3u : component) = ExportRawComponent(ctx, data, component); } } } @@ -410,16 +453,20 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32); continue; } - const auto storeDistance = [&](std::uint32_t variable, std::uint32_t index) { + const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) { if (index == std::numeric_limits::max()) { return; } + if (state.program.Resources().stage == IrShaderStage::Mesh) { + state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, index), f32); + return; + } const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, variable, ConstantU32(state, index)); state.module.AddFunction(spv::OpStore, pointer, f32); }; - storeDistance(state.clipDistanceVariable, output.clipDistance); - storeDistance(state.cullDistanceVariable, output.cullDistance); + storeDistance(StageOutputKind::ClipDistance, state.clipDistanceVariable, output.clipDistance); + storeDistance(StageOutputKind::CullDistance, state.cullDistanceVariable, output.cullDistance); } } @@ -500,8 +547,21 @@ std::uint32_t MeshLoad(SpirvEmitterState& state, std::uint32_t variable, std::ui return value; } +bool IsMeshDistance(StageOutputKind kind) { + return kind == StageOutputKind::ClipDistance || kind == StageOutputKind::CullDistance; +} + std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { - return kind == StageOutputKind::Layer ? TypeU32(state) : TypeF32Vector(state, 4u); + if (kind == StageOutputKind::Layer) { + return TypeU32(state); + } + return IsMeshDistance(kind) ? TypeF32(state) : TypeF32Vector(state, 4u); +} + +std::uint32_t MeshDistancePointer(SpirvEmitterState& state, const SpirvOutputBinding& output, std::uint32_t vertex) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, output.variableId, vertex, ConstantU32(state, output.index)); + return pointer; } } @@ -551,7 +611,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (kind == TessellationAttribute::ControlInput && 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 % 16u / 4u)); + state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, ConstantU32(state, constant % stride / 16u), ConstantU32(state, constant % stride % 16u / 4u)); return pointer; } const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); @@ -624,7 +684,7 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if ((tess.domain != 1u && tess.domain != 2u) || tess.partitioning != 2u || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { + if ((tess.domain != 1u && tess.domain != 2u) || (tess.partitioning != 0u && tess.partitioning != 2u) || (tess.outputTopology != 2u && tess.outputTopology != 3u)) { throw std::runtime_error("unsupported tessellation domain, partitioning, or output topology"); } state.module.EmitCapability(spv::CapabilityTessellation); @@ -632,7 +692,7 @@ void DefineTessellationExecutionModes(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSpacingFractionalOdd); + state.module.AddExecutionMode(state.mainFunc, tess.partitioning == 0u ? spv::ExecutionModeSpacingEqual : spv::ExecutionModeSpacingFractionalOdd); state.module.AddExecutionMode(state.mainFunc, tess.outputTopology == 3u ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw); } } @@ -642,7 +702,30 @@ void DefineMeshOutputs(SpirvEmitterState& state) { throw std::runtime_error("vertex input info is missing for mesh output definition"); } const auto& mesh = state.inputInfo.vertex->mesh; + std::uint32_t clipDistanceCount = 0u; + std::uint32_t cullDistanceCount = 0u; + for (const auto& output : state.outputs) { + if (output.kind == StageOutputKind::ClipDistance) { + clipDistanceCount = std::max(clipDistanceCount, output.index + 1u); + } else if (output.kind == StageOutputKind::CullDistance) { + cullDistanceCount = std::max(cullDistanceCount, output.index + 1u); + } + } for (auto& output : state.outputs) { + if (IsMeshDistance(output.kind)) { + const bool clip = output.kind == StageOutputKind::ClipDistance; + auto& variable = clip ? state.clipDistanceVariable : state.cullDistanceVariable; + if (variable == 0u) { + const auto distances = state.module.Type(spv::OpTypeArray, TypeF32(state), ConstantU32(state, clip ? clipDistanceCount : cullDistanceCount)); + variable = MeshArray(state, spv::StorageClassOutput, distances, mesh.maxVertices); + state.interfaceVariables.push_back(variable); + state.module.AddName(variable, clip ? "gl_ClipDistance" : "gl_CullDistance"); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, clip ? spv::BuiltInClipDistance : spv::BuiltInCullDistance); + } + output.variableId = variable; + output.meshDataVariable = MeshArray(state, spv::StorageClassPrivate, TypeF32(state), state.laneCount); + continue; + } if (output.kind != StageOutputKind::Position && output.kind != StageOutputKind::Parameter && output.kind != StageOutputKind::Layer) { throw std::runtime_error("unsupported mesh output kind"); } @@ -727,7 +810,7 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { } const auto type = MeshOutputType(state, output.kind); const auto value = MeshLoad(state, output.meshDataVariable, spv::StorageClassPrivate, type, ConstantU32(state, half)); - const auto pointer = MeshElement(state, output.variableId, spv::StorageClassOutput, type, index); + 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); } }); @@ -1262,7 +1345,7 @@ void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& ins const auto raised = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeF32(state), raised, GlslStd450(state), GLSLstd450FMax, floating, lowest); const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, state.inputInfo.vertex->tess.partitioning == 0u ? 64.0f : 63.0f)); value = state.module.AllocateId(); state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lowest); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index d9cc99512a..441a136b15 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -428,11 +428,105 @@ struct ScalarType { bool boolean; }; +template +class IdTable { +public: + void Reset(std::size_t bound) { + items.assign(bound, T{}); + present.assign(bound, 0u); + count = 0; + } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + const T* find(std::uint32_t id) const { return contains(id) ? &items[id] : nullptr; } + const T& at(std::uint32_t id) const { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + T& at(std::uint32_t id) { + if (!contains(id)) throw std::out_of_range("prepared SPIR-V id has no entry"); + return items[id]; + } + bool emplace(std::uint32_t id, T value) { + check(id); + if (present[id] != 0u) return false; + items[id] = std::move(value); + present[id] = 1u; + ++count; + return true; + } + T& operator[](std::uint32_t id) { + check(id); + if (present[id] == 0u) { + items[id] = T{}; + present[id] = 1u; + ++count; + } + return items[id]; + } + void erase(std::uint32_t id) { + if (!contains(id)) return; + present[id] = 0u; + items[id] = T{}; + --count; + } + std::size_t size() const { return count; } + bool empty() const { return count == 0; } + class Iterator { + public: + Iterator(IdTable& table, std::uint32_t id) : table(table), id(id) { skip(); } + std::pair operator*() const { return {id, table.items[id]}; } + Iterator& operator++() { + ++id; + skip(); + return *this; + } + bool operator!=(const Iterator& other) const { return id != other.id; } + + private: + void skip() { + while (id < table.present.size() && table.present[id] == 0u) ++id; + } + IdTable& table; + std::uint32_t id; + }; + Iterator begin() { return Iterator(*this, 0u); } + Iterator end() { return Iterator(*this, static_cast(present.size())); } + +private: + void check(std::uint32_t id) const { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + } + std::vector items; + std::vector present; + std::size_t count = 0; +}; + +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + class Specialization { public: explicit Specialization(std::span words) { 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]; + types.Reset(bound); + values.Reset(bound); + resultTypes.Reset(bound); + vectorTypes.Reset(bound); + removed.Reset(bound); 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"); @@ -479,15 +573,15 @@ public: private: std::optional value(std::uint32_t id) const { - const auto found = values.find(id); - return found != values.end() ? std::optional(found->second) : std::nullopt; + const auto* found = values.find(id); + return found != nullptr ? std::optional(*found) : std::nullopt; } std::optional evaluate(const Instruction& instruction) const { const auto op = Opcode(instruction); if (instruction.size() < 4u) return std::nullopt; - const auto type = types.find(instruction[1]); - if (type == types.end() || type->second.width > 32u) return std::nullopt; + const auto* type = types.find(instruction[1]); + if (type == nullptr || type->width > 32u) return std::nullopt; const auto left = value(instruction[3]); if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; @@ -505,8 +599,8 @@ private: case spv::OpBitwiseAnd: return *left & *right; case spv::OpBitwiseOr: return *left | *right; case spv::OpBitwiseXor: return *left ^ *right; - case spv::OpShiftRightLogical: return *right < type->second.width ? std::optional(*left >> *right) : std::nullopt; - case spv::OpShiftLeftLogical: return *right < type->second.width ? std::optional(*left << *right) : std::nullopt; + case spv::OpShiftRightLogical: return *right < type->width ? std::optional(*left >> *right) : std::nullopt; + case spv::OpShiftLeftLogical: return *right < type->width ? std::optional(*left << *right) : std::nullopt; case spv::OpIEqual: return *left == *right; case spv::OpINotEqual: return *left != *right; case spv::OpULessThan: return *left < *right; @@ -532,7 +626,8 @@ private: bool fold() { bool changed = false; bool function = false; - std::map> extracts; + IdTable> extracts; + extracts.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); @@ -554,14 +649,14 @@ private: op = Opcode(instruction); if (op == spv::OpCompositeExtract && instruction.size() == 5u) extracts.emplace(instruction[2], std::pair{instruction[3], instruction[4]}); if (op == spv::OpCompositeConstruct && instruction.size() == 7u) { - const auto first = extracts.find(instruction[3]); - bool shuffle = first != extracts.end() && vectorTypes.contains(instruction[1]) && resultTypes.contains(first->second.first) && vectorTypes.contains(resultTypes.at(first->second.first)); + const auto* first = extracts.find(instruction[3]); + bool shuffle = first != nullptr && vectorTypes.contains(instruction[1]) && resultTypes.contains(first->first) && vectorTypes.contains(resultTypes.at(first->first)); for (std::size_t index = 4; shuffle && index < instruction.size(); ++index) { - const auto found = extracts.find(instruction[index]); - shuffle = found != extracts.end() && found->second.first == first->second.first; + const auto* found = extracts.find(instruction[index]); + shuffle = found != nullptr && found->first == first->first; } if (shuffle) { - const auto source = first->second.first; + const auto source = first->first; bool identity = resultTypes.contains(source) && resultTypes.at(source) == instruction[1]; Instruction replacement = Make(spv::OpVectorShuffle, {instruction[1], instruction[2], source, source}); for (std::size_t index = 3; index < instruction.size(); ++index) { @@ -587,7 +682,8 @@ private: } bool prune() { - std::map> blocks; + IdTable> blocks; + blocks.Reset(bound); std::vector entries; std::uint32_t block = 0; bool entry = false; @@ -622,7 +718,8 @@ private: } return true; }; - std::set loopHeaders; + IdSet loopHeaders; + loopHeaders.Reset(bound); std::map continueHeaders; for (const auto& [label, indices] : blocks) { for (const auto index : indices) { @@ -631,7 +728,8 @@ private: continueHeaders.emplace(instructions[index].at(2), label); } } - std::map> edges; + IdTable> edges; + edges.Reset(bound); for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; auto op = Opcode(terminal); @@ -668,18 +766,19 @@ private: else if (op == spv::OpBranchConditional) successors = {terminal.at(2), terminal.at(3)}; else if (op == spv::OpSwitch) { successors.push_back(terminal.at(2)); - const auto selectorType = resultTypes.find(terminal.at(1)); - const auto stride = selectorType != resultTypes.end() && types.contains(selectorType->second) && types.at(selectorType->second).width == 64u ? 3u : 2u; + const auto* selectorType = resultTypes.find(terminal.at(1)); + const auto stride = selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u ? 3u : 2u; for (std::size_t index = 3; index + stride <= terminal.size(); index += stride) successors.push_back(terminal[index + stride - 1u]); } } - std::set live; + IdSet live; + live.Reset(bound); auto pending = entries; while (!pending.empty()) { const auto label = pending.back(); pending.pop_back(); if (!blocks.contains(label)) throw std::runtime_error("prepared branch references a missing block"); - if (!live.insert(label).second) continue; + if (!live.insert(label)) continue; const auto& successors = edges.at(label); pending.insert(pending.end(), successors.begin(), successors.end()); } @@ -730,14 +829,15 @@ private: return changed; } - std::set referencedIds() const { - std::set referenced; + IdSet referencedIds() const { + IdSet referenced; + referenced.Reset(bound); for (const auto& instruction : instructions) { if (instruction.empty()) continue; std::uint32_t switchLiteralWords = 1u; if (Opcode(instruction) == spv::OpSwitch && instruction.size() > 1u) { - const auto selectorType = resultTypes.find(instruction[1]); - if (selectorType != resultTypes.end() && types.contains(selectorType->second) && types.at(selectorType->second).width == 64u) switchLiteralWords = 2u; + const auto* selectorType = resultTypes.find(instruction[1]); + if (selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u) switchLiteralWords = 2u; } VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { referenced.insert(instruction[index]); }); } @@ -800,7 +900,8 @@ private: } bool propagateCopies() { - std::map copies; + IdTable copies; + copies.Reset(bound); for (const auto& instruction : instructions) { if (Opcode(instruction) != spv::OpCopyObject) continue; if (instruction.size() != 4u) throw std::runtime_error("invalid prepared copy instruction"); @@ -845,7 +946,8 @@ private: } void removeDeadComputations() { - std::map definitions; + IdTable definitions; + definitions.Reset(bound); std::vector pending; bool function = false; for (std::size_t index = 0; index < instructions.size(); ++index) { @@ -871,13 +973,14 @@ private: const auto resultIndex = hasResult ? (hasType ? 2u : 1u) : 0u; for (std::size_t operand = 1; operand < instruction.size(); ++operand) if (operand != resultIndex) pending.push_back(instruction[operand]); } - std::set live; + IdSet live; + live.Reset(bound); while (!pending.empty()) { const auto id = pending.back(); pending.pop_back(); - const auto found = definitions.find(id); - if (found == definitions.end() || !live.insert(id).second) continue; - const auto& instruction = instructions[found->second]; + const auto* found = definitions.find(id); + if (found == nullptr || !live.insert(id)) continue; + const auto& instruction = instructions[*found]; if (!VisitInputs(instruction, [&](std::size_t operand) { pending.push_back(instruction[operand]); })) throw std::runtime_error("missing prepared pure instruction operands"); } for (const auto& [id, index] : definitions) { @@ -890,11 +993,12 @@ private: std::vector header; std::vector instructions; std::vector constants; - std::map types; - std::map values; - std::map resultTypes; - std::set vectorTypes; - std::set removed; + std::uint32_t bound = 0; + IdTable types; + IdTable values; + IdTable resultTypes; + IdSet vectorTypes; + IdSet removed; }; } diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index 514aa328d8..f2878ed016 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -78,9 +78,9 @@ void TranslationContext::vInterpP2F32(const RdnaInstruction& inst) { return base != ShaderPixelInputInfo::NoPixelInputVgpr && inst.source0.reg == base + 1u; }; const auto bit = 1u << inst.source1.value; - if (readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { + if (readsPair(PixelInput::LinearSample) || readsPair(PixelInput::LinearCenter) || readsPair(PixelInput::LinearCentroid)) { program.Metadata().pixelLinearInputs |= bit; - } else if (readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { + } else if (readsPair(PixelInput::PerspectiveSample) || readsPair(PixelInput::PerspectiveCenter) || readsPair(PixelInput::PerspectiveCentroid)) { program.Metadata().pixelPerspectiveInputs |= bit; } } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 0de004a63f..5e108ff580 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -340,7 +340,8 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const auto* ps = options.inputInfo.pixel; const auto vgpr = [&](PixelInput input) { return ps->psInputVgpr[static_cast(input)]; }; const auto loaded = [&](PixelInput input) { return vgpr(input) != ShaderPixelInputInfo::NoPixelInputVgpr; }; - for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, + for (const auto [input, kind] : {std::pair{PixelInput::PerspectiveSample, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::PerspectiveCenter, StageInputKind::BaryCoordSmooth}, + std::pair{PixelInput::PerspectiveCentroid, StageInputKind::BaryCoordSmooth}, std::pair{PixelInput::LinearSample, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCenter, StageInputKind::BaryCoordNoPerspective}, std::pair{PixelInput::LinearCentroid, StageInputKind::BaryCoordNoPerspective}}) { if (!loaded(input)) continue; setVector(static_cast(vgpr(input)), builtin(kind, 0u)); @@ -379,6 +380,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO if (options.userDataBaseRegister >= 8u) { const auto hostLanes = options.inputInfo.vertex->hostSubgroupSize; const std::uint32_t lanes = hostLanes == 1u ? 1u : options.waveSize == 64u && hostLanes == 32u ? 32u : options.waveSize; + entryIr.SetScalarReg(static_cast(2), entryIr.Constant((lanes << 12u) | (lanes << 22u))); entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } setVector(static_cast(5), drawIndex(StageInputKind::VertexIndex)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 5eab8b6736..3e1f574040 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -111,8 +111,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } pixelStorage.psInputVgpr[static_cast(input)] = PixelInputVgpr(pixel.inputAddr, input); }; + place(PixelInput::PerspectiveSample, (pixel.inputAddr & PixelInputBit(PixelInput::PerspectiveSample)) != 0u); place(PixelInput::PerspectiveCenter, pixel.hasPerspectiveCenterVgpr); place(PixelInput::PerspectiveCentroid, pixel.perspectiveCentroid); + place(PixelInput::LinearSample, (pixel.inputAddr & PixelInputBit(PixelInput::LinearSample)) != 0u); place(PixelInput::LinearCenter, pixel.noPerspective); place(PixelInput::LinearCentroid, pixel.linearCentroid); place(PixelInput::PositionX, pixel.posX); @@ -142,7 +144,9 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; pixelStorage.psConservativeZExport = pixel.conservativeZExport; pixelStorage.psOrderedPixelShader = pixel.orderedPixelShader; + if (pixel.ditheredAlphaToCoverage && !pixel.alphaToCoverage) throw std::runtime_error("dithered alpha-to-coverage requires enabled coverage"); pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; + pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; @@ -180,7 +184,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues 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 != 2u || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u) || (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"); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; @@ -189,6 +193,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.partitioning = tessellation->partitioning; target.outputTopology = tessellation->outputTopology; target.patchesPerGroup = tessellation->patchesPerGroup; + target.primitiveGroupSize = tessellation->primitiveGroupSize; + target.breakAtInstance = tessellation->breakAtInstance; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp index 29e92ea915..0b0fd6ae8e 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -53,6 +53,10 @@ IrU1 TranslationContext::readMask(const RdnaOperand& operand) { case RdnaOperandKind::Scc: return IrU1(ir.GetScc()); case RdnaOperandKind::VccZ: return IrU1(ir.LogicalNot(ir.GetVcc())); case RdnaOperandKind::ExecZ: return IrU1(ir.LogicalNot(ir.GetExec())); + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return threadBit(readU32Pair(operand)); default: return IrU1(ir.INotEqual(readRawU32(operand).Value(), ir.Constant(0u))); } } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index 27b6e20f80..f82197547f 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -448,8 +448,7 @@ IrU32 TranslationContext::readScalarCode(std::uint32_t code) { case 235u: case 236u: case 237u: - case 238u: - throw std::runtime_error("TranslationContext::readScalarCode: the scalar aperture source " + RdnaOperandToString(DecodeRdnaScalarSource(code, 0u)) + " is not modelled"); + case 238u: return readRawU32(DecodeRdnaScalarSource(code, 0u)); default: return IrU32(ir.Constant(0u)); } } @@ -482,8 +481,7 @@ IrU32 TranslationContext::readRawU32(const RdnaOperand& operand) { case RdnaOperandKind::SrcSharedBase: case RdnaOperandKind::SrcSharedLimit: case RdnaOperandKind::SrcPrivateBase: - case RdnaOperandKind::SrcPrivateLimit: - throw std::runtime_error("TranslationContext::readRawU32: the scalar aperture source " + RdnaOperandToString(operand) + " is not modelled"); + case RdnaOperandKind::SrcPrivateLimit: return IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand)))); default: throw std::runtime_error("TranslationContext::readRawU32 invalid decoded operand used as a raw U32 source"); } } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index 12a55ce576..cf6c3a789f 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -1,4 +1,5 @@ #include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include #include #include @@ -19,6 +20,10 @@ std::array TranslationContext::readU32Pair(const RdnaOperand& operand) const auto bits = std::bit_cast(static_cast(std::bit_cast(operand.value))); return {IrU32(ir.Constant(static_cast(bits))), IrU32(ir.Constant(static_cast(bits >> 32u)))}; } + case RdnaOperandKind::SrcSharedBase: + case RdnaOperandKind::SrcSharedLimit: + case RdnaOperandKind::SrcPrivateBase: + case RdnaOperandKind::SrcPrivateLimit: return {readU32(operand), IrU32(ir.Constant(static_cast(RdnaApertureSourceValue(operand) >> 32u)))}; default: return {readU32(operand), readU32(offsetOperand(operand, 1u))}; } } diff --git a/core/shader/recompiler/VertexInputSpecialization.hpp b/core/shader/recompiler/VertexInputSpecialization.hpp index 2b2722eed6..12053a78a5 100644 --- a/core/shader/recompiler/VertexInputSpecialization.hpp +++ b/core/shader/recompiler/VertexInputSpecialization.hpp @@ -16,7 +16,7 @@ namespace ShaderRecompiler { inline SharedSpirv SpecializeVertexInputTypes(const CompiledShaderArtifact& artifact, std::span classes) { if (artifact.vertexInputPatches.empty()) return artifact.spirv; - auto words = artifact.spirv.Words(); + auto words = artifact.spirv.Copy(); for (const auto& patch : artifact.vertexInputPatches) { if (patch.location >= classes.size() || classes[patch.location] >= patch.values.size() || patch.word >= words.size()) throw std::runtime_error("invalid prepared vertex type patch"); words[patch.word] = patch.values[classes[patch.location]]; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp new file mode 100644 index 0000000000..c60d90b112 --- /dev/null +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -0,0 +1,422 @@ +#include "ControlFlow/RequestSerializer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr char kHistoricalVersion21[] = + "NVNQQRUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEAAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAABI0VngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAAAAAF4VjQSAQEBOTAAAAA="; +constexpr char kVersion21WithF16Tail[] = + "NVNQQRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAEB"; + +constexpr char kVersion22WithoutF16Tail[] = + "NVNQQRYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQA0DAAE="; + +constexpr char kLayerVersion23[] = + "NVNQQRcAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAA="; + +void Require(bool condition) { + if (!condition) std::abort(); +} + +std::vector DecodeBase64(std::string_view text) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::vector bytes; + std::uint32_t chunk = 0; + std::size_t bits = 0; + for (const char symbol : text) { + if (symbol == '=') break; + const auto value = alphabet.find(symbol); + Require(value != std::string_view::npos); + chunk = ((chunk << 6u) | static_cast(value)) & 0xffffffu; + bits += 6u; + if (bits >= 8u) { + bits -= 8u; + bytes.push_back(static_cast((chunk >> bits) & 0xffu)); + } + } + return bytes; +} + +std::string EncodeBase64(const std::vector& bytes) { + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string text; + for (std::size_t index = 0; index < bytes.size(); index += 3u) { + const std::size_t count = std::min(3u, bytes.size() - index); + std::uint32_t chunk = static_cast(bytes[index]) << 16u; + if (count > 1u) chunk |= static_cast(bytes[index + 1u]) << 8u; + if (count > 2u) chunk |= bytes[index + 2u]; + for (std::size_t symbol = 0; symbol < 4u; ++symbol) { + text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '='); + } + } + return text; +} + +} + +int main() { + const ShaderRecompiler::RequestSerializer serializer; + + const auto historical = serializer.Deserialize(kHistoricalVersion21); + Require(!historical.request.target.float16DenormPreserve); + + const auto version21WithTail = serializer.Deserialize(kVersion21WithF16Tail); + Require(version21WithTail.request.target.float16DenormPreserve); + + bool rejectedTruncatedVersion22 = false; + try { + static_cast(serializer.Deserialize(kVersion22WithoutF16Tail)); + } catch (const std::runtime_error& error) { + rejectedTruncatedVersion22 = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejectedTruncatedVersion22); + + struct CanonicalMain22 { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + constexpr std::array canonicalMain22{{ + {true, false, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkA" + "AAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {true, true, + "NVNQQRYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAAAAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIA" + "AAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, false, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAA==" + }, + {false, true, + "NVNQQRYAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAANO/NqwAAADkAAAAEAAAACQAA" + "AAABeFY0EgEBATkwAAAAAA==" + }, + }}; + for (const auto& fixture : canonicalMain22) { + for (const bool preserve : {false, true}) { + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(request.context.vertex->fetchAttribReg == 9u && request.context.vertex->fetchBufferReg == 13u && request.context.vertex->fetchEmbedded && request.context.vertex->paClVsOutCntl == 0x00050a05u); + Require(!fixture.pixel || (request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle && request.context.pixel->alphaToCoverage && request.context.pixel->stencilRefExportEnable && request.context.pixel->targetOutputMode[0] == 9u && request.context.pixel->targetExportMapping[0] == 0xe4u && !request.context.pixel->layerExported && !request.context.pixel->ditheredAlphaToCoverage)); + Require(request.graphics.has_value() && request.graphics->firstUserSgpr == 6u && request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || (request.graphics->tessellation->patchesPerGroup == 63u && request.graphics->tessellation->domain == 2u && request.graphics->tessellation->outputTopology == 3u)); + 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] = 25u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u}); + if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); + Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + auto truncated = bytes; + truncated.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + if (!fixture.pixel) { + auto vertex23 = bytes; + vertex23[4] = 23u; + const auto vertexReplay = serializer.Deserialize(EncodeBase64(vertex23)); + Require(serializer.Serialize(vertexReplay.request) == EncodeBase64(upgraded)); + continue; + } + for (const bool layer : {false, true}) { + auto layered = bytes; + layered[4] = 23u; + layered.insert(layered.begin() + 258u, static_cast(layer)); + const auto layerReplay = serializer.Deserialize(EncodeBase64(layered)); + Require(layerReplay.request.context.pixel->layerExported == layer && !layerReplay.request.context.pixel->ditheredAlphaToCoverage); + Require(layerReplay.request.target.float16DenormPreserve == preserve && layerReplay.request.graphics->tessellation.has_value() == fixture.tessellation); + Require(!fixture.tessellation || layerReplay.request.graphics->tessellation->patchesPerGroup == 63u); + auto truncatedLayer = layered; + truncatedLayer.pop_back(); + bool layerRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncatedLayer))); + } catch (const std::runtime_error& error) { + layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(layerRejected); + layered[4] = 25u; + layered.insert(layered.begin() + 259u, 0u); + if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); + Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); + } + } + } + + const auto layer23 = DecodeBase64(kLayerVersion23); + Require(layer23.size() == 1451u); + for (const bool layer : {false, true}) { + for (const bool preserve : {false, true}) { + auto bytes = layer23; + bytes[258] = static_cast(layer); + bytes.back() = static_cast(preserve); + const auto encoded23 = EncodeBase64(bytes); + const auto replay = serializer.Deserialize(encoded23); + Require(replay.request.context.pixel.has_value() && replay.request.context.vertex.has_value()); + Require(replay.request.context.pixel->layerExported == layer); + Require(!replay.request.context.pixel->ditheredAlphaToCoverage); + Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); + Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); + Require(replay.request.target.float16DenormPreserve == preserve); + bytes[4] = 25u; + bytes.insert(bytes.begin() + 259u, 0u); + Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); + + auto truncated23 = DecodeBase64(encoded23); + truncated23.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(truncated23))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } + } + + struct Main24Fixture { + bool pixel; + bool tessellation; + std::string_view encoded; + }; + static constexpr std::array canonicalMain24{{ + {true, false, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAAAADTvzasAAAA5" + "AAAABAAAAAkAAAAAAXhWNBIBAQE5MAAAAAE=" + }, + {true, true, + "NVNQQRgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAB" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAJAAAAAAAAAOQAAAAAAAAAAAAAAAAAAAAAAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAkAAAANAAAAAQUKBQABAAAAAAAAAAAABQAAAAAABAAAAAAAAAABAwUHAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACwAAABAAAABAAAAAAQYAAAAAAAAAAAAAAAABBAAAAAQAAAAC" + "AAAAAgAAAAMAAAA/AAAAJQAAAAEANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, false, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAAANO/NqwAAADkAAAAEAAAACQAAAAABeFY0EgEBATkwAAAAAQ==" + }, + {false, true, + "NVNQQRgAAAABAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAgAAAADAAAAAAAAAN+bVxPgrGgk776t3gAA" + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAAA0AAAABBQoFAAEAAAAAAAAA" + "AAAFAAAAAAAEAAAAAAAAAAEDBQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAIAAAALAAAAEAAAAEAAAAABBgAAAAAAAAAAAAAAAAEEAAAABAAAAAIAAAACAAAAAwAAAD8AAAAlAAAAAQA0782rAAAAOQAAAAQA" + "AAAJAAAAAAF4VjQSAQEBOTAAAAAB" + }, + }}; + for (const auto& fixture : canonicalMain24) { + for (const bool preserve : {false, true}) { + for (const bool layer : {false, true}) { + if (!fixture.pixel && layer) continue; + auto bytes = DecodeBase64(fixture.encoded); + bytes.back() = static_cast(preserve); + if (fixture.pixel) bytes[258] = static_cast(layer); + const auto replay = serializer.Deserialize(EncodeBase64(bytes)); + const auto& request = replay.request; + Require(request.context.pixel.has_value() == fixture.pixel && request.context.vertex.has_value()); + Require(!fixture.pixel || (request.context.pixel->layerExported == layer && !request.context.pixel->ditheredAlphaToCoverage)); + Require(request.graphics.has_value() && request.graphics->tessellation.has_value() == fixture.tessellation); + 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] = 25u; + if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, 0u); + Require(serializer.Serialize(request) == EncodeBase64(upgraded)); + } + } + } + + ShaderRecompiler::RecompileRequest request{}; + for (const bool preserve : {false, true}) { + request.target.float16DenormPreserve = preserve; + const auto encoded = serializer.Serialize(request); + const auto replay = serializer.Deserialize(encoded); + Require(replay.request.target.float16DenormPreserve == preserve); + auto bytes = DecodeBase64(encoded); + Require(bytes.size() >= 8u && bytes[4] == 25u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto future = bytes; + future[4] = 26u; + bool futureRejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(future))); + } catch (const std::runtime_error& error) { + futureRejected = std::string_view(error.what()) == "unsupported recompile request serialization version"; + } + Require(futureRejected); + bytes.pop_back(); + bool rejected = false; + try { + static_cast(serializer.Deserialize(EncodeBase64(bytes))); + } catch (const std::runtime_error& error) { + rejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; + } + Require(rejected); + } + + return 0; +} diff --git a/demo/web/index.html b/demo/web/index.html index 74686f5f1e..c280516fb8 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -2,7 +2,7 @@ - + AnyPS5 · Converter diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 5227da96e6..4eaebccf35 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -105,6 +105,7 @@ flowchart LR - 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 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). ### Untracked storage image comparisons diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 82b5576701..ba5c740873 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -28,7 +28,7 @@ git submodule update --init --recursive - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. -- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. +- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. - Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. @@ -140,7 +140,17 @@ The shader preparation threads are traced as well, `shader prepare`, with a buff The `gpu` track holds one `batch` slice per recorder batch, from the batch's timestamp pair. With `APS5_PROFILE_GPU=1` it also holds a slice per command class range, under the class names of the `[gputime]` line (`draw`, `copy`, `fill`, `present-blit`, ...); program ranges are left out. The track keeps up to 262,144 ranges; more set `trace_dropped`. The GPU clock is mapped onto the CPU clock with calibrated timestamps (`VK_KHR_calibrated_timestamps` or the EXT version, enabled only when a trace switch is set) taken when the window opens and when it closes, so the two clocks are matched at both ends of the window. A batch is added when the driver reads its timestamps, after its fence signals, so the batches of the last frames of the window, which complete after it closes, are missing. Without calibrated timestamps the trace has no `gpu` track, and the `[trace]` line says so. -With either switch set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. +With any of the three switches set, the recorder writes a timestamp pair per batch for the whole run, as with `APS5_PROFILE_DRAW`. Without them, a `PerformanceTimer`, a GPU mutex acquisition and a read of batch timestamps each cost one more predictable branch. + +### Summaries for a long session + +Set `APS5_TRACE_SUMMARY_EVERY=` (a whole number above 0; a malformed value fails the first present) to profile a long session in the background without a trace. The first present opens the first interval. Every `` seconds, at the first present at least that long after the interval opened, the presenter appends one line to `aps5-summary.jsonl` in `APS5_TRACE_DIR` or the working directory and opens the next interval at that same present, so the intervals follow each other without a gap and their `frame_ms` cover every interval between presents. The line has the fields of the window summary line for that interval only, plus `interval_start_s` and `interval_end_s` (seconds since the first present). Its `window` is `{"spec":"summary_every ","first_present":,"ms":}` and its `trace` is the empty string. No event buffer is allocated: each thread keeps only the per-scope totals it already folds (a few kilobytes), and the presenter subtracts the totals of the previous interval. An interval that is still open when the title exits writes nothing. + +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. + +`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. ## Driver thread priority diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1a7c6bbe89..9bd067ad5c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -11,10 +11,6 @@ Entries about one library or component are in [debt](debt/), one file per librar - libstdc++-6.dll - libwinpthread-1.dll -### Direct memory - -- Windows direct-memory backing reserves a paging-file section and commits each mapped guest page before setting its protection. Committed physical contents remain in the section across unmap/remap and are released when the backing and its remaining views are destroyed; commitment is not recovered merely by unmapping a page. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - ### Silent stubs Throughout the project, every function at every stage either **does exactly what it's supposed to or throws an exception**. Everywhere... except: @@ -25,7 +21,7 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters ### Functional diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index cf540b7c52..2a41360807 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -38,3 +38,4 @@ - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7c5f2193e9..111c9e39ff 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,10 +2,12 @@ ### Unknown function info +- [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. - [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 - [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 -- [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw +- [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. +- [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw - [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 @@ -27,11 +29,16 @@ ### Functional -- [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 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 matching view still throws when it has another dimension (2D array of more than one layer or from another layer, cube) or 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. +- [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 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. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. + +- [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 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 from slice 0 is a storage image over all its slices (`LayeredDepthStorage`): 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, 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, or a sampled copy) is a separate image, 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. - [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. Planes with mips, a base slice other than 0, 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). +- [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. +- [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. @@ -44,9 +51,10 @@ - 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). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. -- [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 or the image copy. Results over a range that left the watch after they became pending (a host import that does not stay watched, made while they were pending) have no saved bytes, and storing them throws; saving those bytes or storing the results when the range leaves the watch is unimplemented. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: 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. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. +- [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 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 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. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: 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 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, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; 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. +- [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 takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (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) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [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. @@ -71,13 +79,14 @@ - [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. - [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 mipmapped, 3D or array color target throw. 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 -- [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. -- [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 only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. 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. +- [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 mipmapped (base mip 0), 3D or array color target throw. 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). +- [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. +- [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. 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. +- [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. +- [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 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 is modeled. 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, 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. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [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. @@ -85,6 +94,7 @@ - [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. - [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. The output is SDR, so 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 tone-map luminance sceSystemServiceGetHdrToneMapLuminance reports. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. - [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 @@ -92,6 +102,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [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. +- [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. - [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). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, 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) @@ -111,3 +122,4 @@ - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [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. - [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. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. +- [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 2 GiB (imports and type-3 mirrors share it; #167's rule). 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 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. diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 3a3e8f5009..d9a2f57845 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -6,7 +6,7 @@ - [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 - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts -- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. Only 48000 Hz and mapping families 0 and 1 are implemented; other rates and families throw +- [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is unknown and throws - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 1c9129423e..957dc51912 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -8,5 +8,5 @@ ### 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. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified +- [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. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 7d7b5dfcc7..017fe07cf4 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -19,7 +19,7 @@ ### Functional -- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. `sceAudioOut2DisableChat` is not exported, as no title imports it. +- [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [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. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device 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. - [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/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 11cddd4a6b..99cf6f6734 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -3,3 +3,5 @@ ### Silent stubs - [sceCesMbcsUcsContextInit](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns success without writing to the context, and 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); the context layout and its size are unknown, so nothing is stored. PPSA15246 calls it and imports no other function that takes the context; no conversion through an initialized context exists. `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 +- [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) + diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md new file mode 100644 index 0000000000..1f1b491f21 --- /dev/null +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -0,0 +1,9 @@ +# libSceConvertKeycode technical debt + +### 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; a value other than 0 throws, and so does a keyboard type other than 0. + +### Functional + +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) takes the keycode as a USB HID keyboard usage and returns the unshifted character of the US layout for letters, digits, punctuation, space and the keypad. A keycode without a character returns 0 with the character 0; whether the console returns an error instead is not measured. diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index bea722d39b..0a64c52ca5 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -1,5 +1,9 @@ # libSceFiber technical debt +### Functional + +- [sceFiberGetInfo](../../../core/libs/prx/libSceFiber/Export.cpp) reports the requested-size window at the top of the host stack used for execution, rather than the original caller buffer, which is retained and pinned. The additional 1 MiB host-call margin below that window is not included in `size_context`. Planet Coaster (PPSA01736 v01.017) uses this range for ChakraCore garbage-collector stack scans; reporting the unused caller buffer makes the scan cross unmapped memory. The range is covered by a regression test while running, switching and migrating a fiber between threads. Console behavior for substituted host stacks has no direct equivalent and is not measured. + ### Unknown function info - [_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 diff --git a/docs/dev/debt/libSceHttp2.md b/docs/dev/debt/libSceHttp2.md index ec43ab5de9..3486ea1fd3 100644 --- a/docs/dev/debt/libSceHttp2.md +++ b/docs/dev/debt/libSceHttp2.md @@ -14,4 +14,4 @@ ### 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. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw +- [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/libSceIme.md b/docs/dev/debt/libSceIme.md index 37286b5b12..8511d88e3d 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -1,8 +1,13 @@ # libSceIme technical debt +### 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 + ### Unknown function info -- [sceImeGetPanelPositionAndForm](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title +- [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 ### Functional diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 0a303516f1..7cc67d0d95 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -14,3 +14,4 @@ - [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) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. 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 +- [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323dc..86c1b99a3f 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -3,11 +3,15 @@ ### Unknown function info - PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_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. -- [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; nothing is allocated from these pools, so the peak and current use are 0, where the console already reports use right after initialization (0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool) +- [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 - [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters ### Functional +- [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` +- [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. - [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 - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does +- [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index ae86494224..1eda352489 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -18,9 +18,10 @@ - [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. Lock and unlock take the same lock as `sceNgs2RackLock` - [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 -- [sceNgs2VoiceGetPortInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet +- [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. - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented +- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 148b2634e6..9e8e3461c6 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -1,5 +1,9 @@ # libSceNpWebApi2 technical debt +### Silent stubs + +- [sceNpWebApi2SetRequestTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - the `(request id, timeout)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api2/np_web_api2.cpp). Pinball FX (PPSA08755) calls it while its main menu loads. It succeeds for any request id and stores nothing: no request reaches a service, as `sceNpWebApi2SendRequest` fails with `UNAVAILABLE` at once, so no timeout can expire. shadPS4 returns `LIB_CONTEXT_NOT_FOUND`, `USER_CONTEXT_NOT_FOUND` or `REQUEST_NOT_FOUND` for an id it does not know; this library keeps no context or request table (`sceNpWebApi2CreateRequest` hands out counter values, and `sceNpWebApi2AbortRequest`, `sceNpWebApi2AddHttpRequestHeader` and `sceNpWebApi2DeleteRequest` also accept any id), so those errors are not reported until the request functions track their ids together + ### Unknown function info - [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. 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. diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 535f40cfa7..74c417e67e 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -3,3 +3,4 @@ ### Functional - [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. 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. +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 8cfd5341d6..04435c2849 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -8,3 +8,4 @@ - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 0 to 3 are accepted and stored; the meaning of mode 3 is 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 diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 3a13df5e03..ecf89fb032 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,4 +2,4 @@ ### 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 every chunk id is accepted while `scePlayGoGetChunkId` lists only chunk 0; the console's answer for an id outside the package's chunk table is not checked. +- [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, and a package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts only chunk 0 and returns `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` for any other id; 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; the two answers above are unverified on a console, as is the console's answer for an id outside a package's chunk table. diff --git a/docs/dev/debt/libScePosixForWebKit.md b/docs/dev/debt/libScePosixForWebKit.md index cf5516d57a..c57da143e9 100644 --- a/docs/dev/debt/libScePosixForWebKit.md +++ b/docs/dev/debt/libScePosixForWebKit.md @@ -9,3 +9,4 @@ - [mkstemp and isatty](../../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes of host OS entropy (`getentropy`, `rand_s` on Windows), as `arc4random_buf` does, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent - [fnmatch](../../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws +- [getservbyname](../../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) searches a built-in copy of 37 lines of FreeBSD 11.4's [etc/services](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/etc/services) (ftp-data, ftp, ssh, telnet, smtp, domain, tftp, http, pop3, ntp, imap, https, rtsp, submission, imaps, pop3s with their aliases and protocols, in file order) the way [getservent.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/net/getservent.c) searches the file: case-sensitive name or alias, a null protocol takes the first match, the port in network byte order, the result in per-thread storage. The console's `/etc/services` is unknown, so any other service returns null without setting `errno`, as for a name the file lacks. `getaddrinfo` still resolves service names through the host's service database diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 45da9c14c8..6759fd2a9e 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -7,10 +7,12 @@ - [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. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`) report `SAVE_DATA_BLOCKS_MAX` for both. 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. A save's size is the `blocks` of the `sceSaveDataMount3` call that created it, kept beside the save in `.blocks`; a save without a kept size takes that of its next mount that names one, and otherwise reports `SAVE_DATA_BLOCKS_MAX`. The search infos (blocks, free blocks and 32 reserved bytes per returned name, following shadPS4's `OrbisSaveDataSearchInfo`), `sceSaveDataGetMountInfo` and the two block keys use that size and, as free, what the save's files leave of it, each file rounded up to 32 KiB blocks; the console's file system overhead is not known and not counted. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it. In the directory name pattern `%` matches any run of characters, none included, and `_` exactly one, as the comments of shadPS4's matcher describe; also not confirmed on hardware - [sceSaveDataInitialize3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown ### Functional +- [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. +- [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` diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 0a72ff2bfa..f5b9ed5354 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,11 +2,12 @@ ### Unknown function info +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. 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`. 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. 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) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetCaList, sceSslFreeCaList, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and the 16-byte `SceSslCaList` layout (an array of certificate handles and a 32-bit count) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs) and shadPS4's `OrbisSslCaList`, not confirmed for the PS5. On the console the list holds the certificate authorities the SSL service has; [shadPS4](https://github.com/shadps4-emu/shadPS4/pull/4104) returns a placeholder certificate from `sceSslGetCaCerts` because Rise of the Tomb Raider fails its initialization checks without one. No root certificate bundle is shipped and no loaded certificate is kept, so here a context holds none: `sceSslGetCaList` clears the list and returns `NOT_FOUND`, as `sceSslGetCaCerts` does, `sceSslFreeCaList` accepts only that cleared list, and `sceSslUnloadCert` (imported by PPSA15508) has nothing to drop and is assumed to return 0. A null list returns `0x8095177A`, which `sceSslGetCaCerts` and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/libs/network.cpp) return for a null argument (shadPS4, PS4, returns `0x8095F007`). An unknown context throws (KytyPS5 returns `SSL_ERROR_INVALID_ID`, `0x8095F006`, from `sceSslGetCaCerts`), and so does a list `sceSslGetCaList` did not return -- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). They act on a connection, which `sceSslCreateConnection` does not create yet, so they throw until connections exist +- [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 readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer +- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). Connection storage exists, but their signatures and option behavior remain unknown, so they still throw ### Functional -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslEnableVerifyOption](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index a20f0876af..ce195aa306 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,7 +9,7 @@ ### Unknown function info - [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 -- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. +- [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../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 @@ -21,5 +21,7 @@ ### Functional +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle - [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 - [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. +- [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 diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 124436d2ad..b40ba0c49a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,7 @@ ### Unknown function info +- [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. - [_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. - [_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. @@ -11,7 +12,8 @@ - [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 - [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` -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (an exhausted guest arena, an unknown pointer) throw, as they do for `realloc` +- [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 +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol @@ -20,6 +22,8 @@ - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s and memmove_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` +- [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified @@ -28,6 +32,7 @@ ### Functional +- 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. - [getc_unlocked, putc_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. - [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 - [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 @@ -44,7 +49,7 @@ - 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. - [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 +- [`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 - 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. @@ -55,14 +60,17 @@ - [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. 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. Its 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. - [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 - [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. +- [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` +- [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 - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written - [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 - [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)) - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. 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 -- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../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 -- [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 host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. +- [_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 +- [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 +- [_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. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b54b27aba9..f947d86c51 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,8 +13,11 @@ - [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`. - [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it -- [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. +- [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 +- [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. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_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 - [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 - [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 @@ -22,6 +25,7 @@ - [sceKernelGetAppInfo](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - only `app_id` and the title ID (offset 0x10, up to 9 characters plus a terminator) are written; callers declare the structure with different sizes, so the rest is left alone. `app_id` is 1, the running title's id in libSceSystemService, which is a guess, not something measured. The error codes follow shadPS4: `EPERM` for another process, and 0 without writing anything for a null pointer. - [AMPR object layout and compact gathers](../../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. - [APR resolve ForEach result](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - `sceKernelAprResolveFilepathsToIdsAndFileSizesForEach` returns the number of paths that resolved, with the per-path status in `results` and the failed ids/sizes set to the invalid id and 0. The [ampr_emu](https://github.com/drakmor/ampr_emu) bridge passes the count of its resolver through and does not show what is counted; counting successes is not confirmed on a title. The guest errno of the failing non-`ForEach` resolves is set through `__error_nid_postfix` +- [APR reads past the end of a file](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - a read that reaches the current end of the file on disk copies the bytes the file has and fills the rest of the destination with zeros; a read that comes back short before that end still throws. PPSA18605 resolves its settings file once, rewrites it one byte shorter and reads it again with the size that resolve returned. The console's behaviour for a read past the end of a file is unverified: it may zero-fill, leave the destination untouched or fail the command - [AMPR counter access](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output @@ -31,14 +35,16 @@ - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signature - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows +- [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call - [sceKernelGetModuleInfoFromAddr](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted. The input `st_size` is ignored, because PPSA21703's `libc.prx` passes an uninitialized 0x1A8-byte buffer; the full size is written back. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. On Windows the module is the PE image holding the address: a relinked image reports its `.elf` sections as segments and the eh_frame_hdr recorded in `.ehmeta`, any other image its first 4 sections with memory access and the `.ehfram` section without an eh_frame_hdr; the TLS fields and `init_proc_addr`/`fini_proc_addr` are 0 - [sceKernelGetModuleList](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [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 448 MiB flexible budget while they are loaded, for this size and for the flexible maps, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165) 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)) +- [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)) - [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 +- [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` passes `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 waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [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 @@ -54,8 +60,8 @@ - [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 - [_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 -- [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` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors throw on Windows and are polled through the host on Linux. Dungeons (PPSA16064) selects on descriptor 0 -- [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, except on Windows where it still throws; 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. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [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` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; 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. 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 @@ -90,12 +96,13 @@ - [sched_get_priority_max, sched_get_priority_min](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` and `scePthreadSetschedparam` accept those two policies with a priority of 256 to 767 or 896 to 959 and keep the policy, as measured; a created thread takes the policy of explicit scheduling attributes, or its creator's when it inherits. When it inherits, it still takes the priority of its attributes, where the console gives it its creator's priority - [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) - throws: 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 neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. 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` ### Functional - [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 is unsupported, as for `shm_open`. 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 the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), 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) and a negative `nevents` throw. 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 -- [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`. 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; 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 +- [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`. 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 - [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`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond @@ -111,7 +118,7 @@ - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 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, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; 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, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console +- [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist - [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`) @@ -125,17 +132,21 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it - [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`); here a joined thread is released, so a second join reads freed memory. 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 - [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. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_setaffinity`. 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 - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_getaffinity`. 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. - [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. - [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. -- [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host CPU's name, 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`) +- [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, because Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE without reporting them through CPUID +- [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_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK`, `RLIMIT_NPROC` and `RLIMIT_NOFILE` 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 only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. - [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. @@ -143,9 +154,11 @@ - [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. - [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). - [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, which the guest can neither ignore nor suppress: `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; `SO_NOSIGPIPE` is not an option here, and raises nothing 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, libkernel `fstat` on a socket throws, 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` +- [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. `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 closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) 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 - [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 only, 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). Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_NOSIGPIPE`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them) and every other level (`IPPROTO_TCP`, `IPPROTO_IP`, `IPPROTO_IPV6`) fails 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 - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified @@ -157,7 +170,7 @@ - [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`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). 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, where `F_GETLK` with `F_UNLCK` throws. 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 -- [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, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [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 - [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. @@ -166,5 +179,8 @@ - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. -- [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - reports 2048 page-table entries on each side (a 4 GiB pool of 2 MiB entries, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/kernel/memory.cpp)) and all of them available: no entries are counted as used, so a title never sees page-table pressure. KytyPS5 counts the 2 MiB entries of the committed CPU- or GPU-accessible ranges as used, which this port does not do. The four `int*` arguments follow OpenOrbis' `libkernel.h`, the Orbital type database and SharpProspero; a null pointer returns `SCE_KERNEL_ERROR_EFAULT` as in KytyPS5. None of the values is measured on a console. PPSA30601 (Resonance) calls it during start-up +- [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 page-table entries over the recorded protection ranges (committed mappings; reservations are not recorded): each range charges one entry per 2 MiB granule it spans to the cpu pool when it has CPU access bits (0x7) and to the gpu pool when it has GPU access bits (0x30), so a range with both counts in both and two ranges sharing one granule charge it once each, as KytyPS5 sums per committed range. Mappings with no access bits, such as PROT_NONE or ACP-only protections, charge neither pool. The 2048-entry budget per side, its constants, and whether the console counts reservations or guest image segments are unverified. Resonance: A Plague Tale Legacy (PPSA30601) reaches this call after the other missing exports of its start-up are provided - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known +- [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled +- [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 diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 67852898ab..7922600c43 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,12 +4,14 @@ - [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, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). +- 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. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - `--frame-red-zone` frames only leaf functions that start with a chain of pushes of at least five bytes (`push rbp; mov rbp, rsp; push ...`, or pushes alone) and leave through `pop ...; ret` exits of at least five bytes ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). In a leaf without a frame pointer, every RSP operand is moved by the 128 bytes RSP is lowered by, so the leaf is skipped when one cannot move in place: an operand at or above RSP with an 8-bit displacement, or one without a displacement. Leaves without pushes, shorter prologues and exits, and functions with indirect jumps or other uses of RSP keep their red zone. Inside a moved frame the CFA rules of the guest's unwind tables are 128 bytes off. Function starts are direct call targets and 16-byte-aligned code after int3 padding, so a leaf reached only through a pointer without padding before it is not seen. A body ends at int3 padding, at 16-byte-aligned code after a ret or ud2, and at the target of a jmp with only padding between them (a tail call to the next function), so such a leaf is not taken into the body before it there; one that follows a jmp to elsewhere, such as a loop's back edge, without int3 padding still is. - 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. -- 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. The `unused-filter=1` and `unused-filter=2` readers parse the program headers themselves and do not load it. +- 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 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. +- 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` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or 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)). - 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)). Rosetta runs AVX2, FMA, BMI1/2, F16C and MOVBE but reports none of them through CPUID, so code that checks CPUID takes its baseline path. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. 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; that the converted code runs under Rosetta was checked on an M5 Max only, outside this repository's CI. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index de34303b91..036f899098 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -14,10 +14,12 @@ - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them, so a shader with eight guest clip and cull distances exceeds the eight the device holds and fails to compile. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan +- [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) ### Functional - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. +- [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Computed pointers, branching prologs, fragment epilogs and continuations reading `s_getpc_b64` remain unsupported - The [resource tracker](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans a [descriptor source](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) used only behind a conditional branch as inactive when the branch condition reads only clean (specialization) memory and so is evaluable at capture time; an inactive source is bound as zero words and not validated. Other conditions (EXEC, VCC, SCC set by vector or non-clean data, loop headers) leave every source active, as before, and a branch on a value that changes between dispatches is a specialization key like any other clean slot. PPSA15246 has a copy kernel that selects a buffer or an image path from an SRT word and reads the same address as a V# or a T#; before, the untaken path's descriptor was validated and threw. Whether the console would also accept an invalid descriptor in a path it does not take is assumed, not measured - [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. @@ -50,8 +52,7 @@ - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. - [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; 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. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, explicit LOD, bias, offsets, derivatives, comparison) still throw. - [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 -- [Scalar aperture base sources](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) - `src_shared_base`, `src_shared_limit`, `src_private_base` and `src_private_limit` (scalar source codes 235 to 238) are not modelled: decoding throws `unsupported scalar source operand code` with the code and the program counter, so a shader that reads one does not compile. Operand 235 (`src_shared_base`) was reported on [#1182](https://github.com/boykopovar/AnyPS5/pull/1182) from PPSA26344; the flat aperture routing does not read the base, so a shader that builds its own flat addresses from it stops at decode. -- [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 +- [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 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) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes. @@ -83,11 +84,13 @@ - [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. -- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of fractional-odd triangle patches that are NaN tessellate as 1, as in the D3D11 reference tessellator; the console is not measured. +- [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer or fractional-odd triangle patches that are NaN tessellate as 1, 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. - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. +- [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) and fractional-even (3) partitioning throw. +- 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, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; 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 [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). @@ -98,6 +101,7 @@ - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given), and `ds_add_f32`, which keeps the host's `OpFAdd`. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment and atomics throw. - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. @@ -106,10 +110,14 @@ - [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). - [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, compare, hash, store) throws, apart from the overwrites 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`. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. -- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not +- The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. +- [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [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 may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. 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 a zero-initialised function-scope array 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; the host compiler decides whether that array stays in registers or spills to scratch, which 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 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. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 950c7b0b4e..d61e36d70a 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -37,7 +37,13 @@ The same file also accepts these settings, each at most once, in the form `Setti | Setting | Value | Default | |---------|-------|---------| | `LeftStickDeadzone`, `RightStickDeadzone` | Whole percentage from 0 to 90 of the controller stick's travel that reads as centered. Outside it the travel is rescaled, so the stick still reaches its full range. Keyboard and mouse stick bindings are not affected. | `0` | -| `MouseSensitivity` | Number greater than 0 and at most 10 that scales mouse movement on the right stick while the mouse is captured (`ToggleMouse`). | `1` | +| `MouseSensitivity` | Number greater than 0 and at most 10. At 1, moving the mouse 3000 counts per second tilts the stick fully, about 5 cm per second at 1600 DPI; a larger number needs less movement. | `1` | +| `MouseVerticalScale` | Number from 0.1 to 10 that multiplies vertical mouse speed, for games that pitch slower or faster than they turn. | `1` | +| `MouseCurve` | Number from 0.2 to 5, the response exponent. Above 1 gives finer control at low speed; below 1 reaches large tilts sooner. | `1` | +| `MouseDeadzoneOffset` | Number from 0 to 0.9, the smallest tilt any movement produces as a fraction of full tilt. Set it just above the game's own deadzone: raise it if slow movement doesn't move the camera, lower it if small movements jump. | `0.2` | +| `MouseMaxSpeed` | Number from 0.1 to 10, the highest tracked speed as a multiple of the full-tilt speed. Above 1, a fast flick keeps the stick at full tilt a little longer after the mouse stops. | `1` | +| `MouseSmoothingMs` | Number from 0 to 500, the smoothing time constant in milliseconds. 0 follows every mouse report directly. | `10` | +| `MouseDecayMs` | Number from 1 to 1000, the milliseconds without movement after which the stick is centered. | `30` | ```ini LeftStickDeadzone = 12 @@ -45,4 +51,20 @@ RightStickDeadzone = 8 MouseSensitivity = 2.5 ``` +## Mouse as the right stick + +`ToggleMouse` (the middle mouse button by default) captures the mouse and drives the right stick from how fast the mouse moves. Capture uses raw relative input, so the pointer speed and acceleration set in the operating system don't apply. Press it again to release the mouse. Capture also ends, and every held key is let go, when the window loses focus. + +The stick tilts in the direction the mouse moves, and further the faster it moves. Speed is measured in counts per second over the time between updates, so the same hand speed gives the same tilt at any frame rate. Any movement tilts the stick at least `MouseDeadzoneOffset`, so slow movement isn't lost inside the game's own deadzone. When the mouse stops, the stick eases back and is centered `MouseDecayMs` after the last movement. + +For example, for a game with a small deadzone that needs fast turns: + +```ini +MouseSensitivity = 1.6 +MouseDeadzoneOffset = 0.12 +MouseCurve = 1.3 +``` + +Earlier versions sampled the mouse every 33 ms and tilted the stick at least halfway for any movement. `MouseDeadzoneOffset = 0.5`, `MouseSmoothingMs = 0` and `MouseDecayMs = 33` come close to that behavior. + An invalid line reports the file and line number and stops input initialization. If the configured file does not exist or cannot be read, AnyPS5 reports an error. With no `anyps5-input.ini` and no `ANYPS5_INPUT_CONFIG`, the built-in mapping is used. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index bf27e8f6ff..2aa5efb27a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -37,6 +37,8 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. `--to-intel` rewrites AMD-specific instructions found in the executable and bundled modules; it cannot translate AMD-specific instructions generated by a title at runtime, so such titles require an AMD host. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. +Titles run natively, so the host CPU needs the extensions of the PS5's Zen 2 CPU that compiled code uses without checking, among them AVX2, FMA, F16C, BMI1, BMI2, LZCNT and MOVBE. On Linux and Windows a converted title logs the host CPU's extensions at start-up and stops before running any title code, naming the CPU and the missing extensions, if one is absent ([list](../dev/debt/libkernel.md)). + The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options @@ -52,6 +54,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--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`. | +| `--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`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | | `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | @@ -78,7 +81,11 @@ The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, ## Runtime layout -For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires a fixed address. Without it, Windows placement and relocation behavior are unchanged. This option does not change Linux or macOS output. +For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output. + +For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux. Use this option only when the executable requires a fixed address. Without it, Linux output is a position-independent executable as before. + +The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased. Paths are relative to the output executable: @@ -98,9 +105,9 @@ Use the generated files printed as `Guest module:` for bundled title modules. `l On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. -On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed in full when the title allocates it, not when its pages are first used. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover it together with all other committed memory. Otherwise the allocation throws `create direct memory backing of 0x bytes ( MiB)` with the Windows error; enlarge the page file or close other applications. +On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. -On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. Ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. +On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the range does not start on the device's sparse block size in the backing file, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. Linux: @@ -172,6 +179,10 @@ most called traced imports (1 calls in total): Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. +### Windows crash diagnostics + +Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. + ## Exit codes `0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. diff --git a/tools/check_compile.py b/tools/check_compile.py new file mode 100644 index 0000000000..1ec01a433c --- /dev/null +++ b/tools/check_compile.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import shlex +import shutil +import subprocess +import sys +from pathlib import Path + +CPP = (".cpp", ".cc", ".cxx", ".c") +DROP = ("-c", "-MD", "-MMD", "-MP") +DROP_WITH_VALUE = ("-o", "-MT", "-MF") + + +def capture(command, cwd=None): + return subprocess.run(command, capture_output=True, text=True, encoding="utf-8", + errors="replace", cwd=cwd) + + +def repository(): + result = capture(["git", "rev-parse", "--show-toplevel"]) + if result.returncode != 0: + raise ValueError(result.stderr.strip() or "not a git checkout") + return Path(result.stdout.strip()) + + +def entries(build): + if not build.is_dir(): + raise ValueError(f"{build} is not a directory; configure the build first") + manifest = build / "compile_commands.json" + if manifest.is_file(): + return json.loads(manifest.read_text(encoding="utf-8")) + 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"]) + if result.returncode != 0: + raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") + return json.loads(result.stdout) + + +def entry_key(entry): + directory = Path(entry["directory"]).resolve() + source = Path(entry["file"]) + if not source.is_absolute(): + source = directory / source + source = source.resolve() + common = Path(os.path.commonpath([str(directory), str(source)])) + return os.path.relpath(source, common).replace(os.sep, "/") + + +def index(build): + commands = {} + for entry in entries(build): + commands.setdefault(entry_key(entry), entry) + return commands + + +def changed(root, base): + result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", base]) + if result.returncode != 0: + raise ValueError(f"git diff {base}: {result.stderr.strip()}") + untracked = capture(["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "-z"]) + if untracked.returncode != 0: + raise ValueError(f"git ls-files: {untracked.stderr.strip()}") + names = dict.fromkeys(result.stdout.split("\0") + untracked.stdout.split("\0")) + return [name for name in names if name.endswith(CPP)] + + +def syntax_only(entry, source): + arguments = shlex.split(entry["command"]) + directory = Path(entry["directory"]) + wanted = source.resolve() + trimmed = [] + index = 0 + while index < len(arguments): + argument = arguments[index] + if argument in DROP_WITH_VALUE: + index += 2 + continue + if argument in DROP: + index += 1 + continue + if not argument.startswith("-"): + candidate = Path(argument) + if not candidate.is_absolute(): + candidate = directory / candidate + if candidate.resolve() == wanted: + index += 1 + continue + trimmed.append(argument) + index += 1 + trimmed += ["-fsyntax-only", str(wanted)] + return trimmed + + +def check(root, build, files): + commands = index(build) + checked = 0 + failed = 0 + for file in files: + source = file.resolve() + name = os.path.relpath(source, root).replace(os.sep, "/") + entry = commands.get(name) + if entry is None: + print(f"{name}: no compile command in {build}", file=sys.stderr) + continue + checked += 1 + result = subprocess.run(syntax_only(entry, source), cwd=entry["directory"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + continue + failed += 1 + output = (result.stdout or "") + (result.stderr or "") + sys.stderr.write(output if output.endswith("\n") else output + "\n") + return checked, failed + + +def main(argv=None): + parser = argparse.ArgumentParser( + 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("--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") + args = parser.parse_args(argv) + try: + root = repository() + files = [Path(path) for path in args.paths] or [root / path for path in changed(root, args.base)] + if not files: + print(f"no changed translation unit against {args.base}") + return 0 + checked, failed = check(root, args.build, files) + except (json.JSONDecodeError, OSError, ValueError) as error: + parser.error(str(error)) + if failed: + print(f"{failed} of {checked} translation units did not compile", file=sys.stderr) + return 1 + if not checked: + print(f"no translation unit to check in {args.build}", file=sys.stderr) + return 1 + print(f"{checked} translation units compile") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 34b639f166..61b187c984 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -6,9 +6,12 @@ import re import subprocess import sys from collections import defaultdict -from pathlib import PurePosixPath +from pathlib import Path, PurePosixPath from urllib.parse import unquote +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from nid_names import CPP_TOKEN + RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md or docs/dev/debt/ in the same pull request; #endif and namespace ends are always allowed"), "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed under Silent stubs in docs/dev/debt/.md, by their file or by name with a path in their library"), @@ -36,7 +39,6 @@ DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') -TOKENS = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'|//[^\n]*|/\*.*?\*/', re.S) ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" @@ -45,7 +47,7 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\]\(([^)\s]+)") -SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) +SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") @@ -76,7 +78,7 @@ class Check: self.numstat[path] = (added, deleted) paths = iter(self.status) path, number = None, None - for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).splitlines(): + for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).split("\n"): if line.startswith("diff --git "): path, number = next(paths), None elif line.startswith("@@"): @@ -99,9 +101,10 @@ class Check: exports = {} for path, lines in self.files(lambda p: p.suffix in CPP): core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts - source = None + source = self.show(path).split("\n") + masked = cpp_text("\n".join(source), preserve_comments=True).split("\n") for number, text in lines: - bare = STRINGS.sub('""', text) + bare = masked[number - 1] if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): self.report("comment", path, number) if EXTENSION.search(bare): @@ -109,7 +112,7 @@ class Check: if core and TITLE.search(text): self.report("title-specific", path, number, TITLE.search(text).group(0)) match = EXPORT.search(bare) - if match and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): + if match and core and path.startswith("core/libs/prx/") and not match.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): source = source or self.show(path).splitlines() body = function_body(source, number - 1) if body is None: @@ -130,11 +133,11 @@ class Check: return pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" result = subprocess.run(["git", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") - for line in result.stdout.splitlines(): + for line in filter(None, result.stdout.split("\n")): _, path, number, text = line.split(":", 3) name = EXPORT.search(text).group(1) own, own_number = exports[name] - if library(path) != library(own) and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(self.show(path).splitlines(), int(number) - 1) is not None: self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): @@ -331,9 +334,18 @@ def allowed_notes(path): return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) +def cpp_text(text, preserve_comments=False): + def mask(match): + if match.group("code") is not None or preserve_comments and match.group("comment") is not None: + return match.group() + blank = re.sub(r"[^\n]", " ", match.group()) + return blank if match.group("comment") is not None else '""' + blank[2:] + + return CPP_TOKEN.sub(mask, text) + + def function_body(lines, start): - text = "\n".join(lines[start:start + 400]) - text = TOKENS.sub(lambda match: "" if match.group(0).startswith("/") else '""', text) + text = cpp_text("\n".join(lines[start:start + 400])) brace, semicolon = text.find("{"), text.find(";") if brace < 0 or 0 <= semicolon < brace: return None diff --git a/tools/check_skips.py b/tools/check_skips.py index e669234ee6..0cc51cacb3 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -7,9 +7,22 @@ DEVICE_LIMITATION = re.compile(r"skipped,.*(subgroup|the device (has no|reports| NO_DISPLAY = re.compile(r"skipped,.*no display", re.IGNORECASE) +HIGH_REFRESH_LIMITATION = re.compile(r"current display does not provide 120 Hz", re.IGNORECASE) + WINDOWS_PREFIX = "agc" +def high_refresh_limitation(output): + reasons = [] + for line in output.splitlines(): + reason = line.strip() + if reason.lower().startswith("skipped,"): + reason = reason[len("skipped,"):].strip() + if reason: + reasons.append(reason) + return bool(reasons) and all(HIGH_REFRESH_LIMITATION.fullmatch(reason) for reason in reasons) + + def skips(path): for case in ET.parse(path).getroot().iter("testcase"): if case.get("status") == "disabled": @@ -41,11 +54,11 @@ 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)] - expected = f"any {WINDOWS_PREFIX}* test, which the runner has no Vulkan driver for, or a test that reports no display or Vulkan device" + 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)] + 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)] - expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension or no display for a Vulkan window" + unexpected = [name for name, output in found if not DEVICE_LIMITATION.search(output) and not high_refresh_limitation(output)] + expected = "any test that reports the device limitation it needs, such as narrow subgroups, a missing extension, no display for a Vulkan window or a display without a 120 Hz mode" for name in unexpected: print(f"error: {name} skipped") if unexpected: diff --git a/tools/debt_split.py b/tools/debt_split.py index bfb7f9d522..e7341fefe1 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -163,14 +163,14 @@ def apply_change(root, base, new, index_text): _, section, item, name = entry entries = load(name).setdefault(section, []) moved = relink(item, INDEX, debt_file(name)) - position = len(entries) + position = 0 if anchor is None else len(entries) if anchor is not None and anchor[1] == section and anchor[3] == name: anchored = relink(anchor[2], INDEX, debt_file(name)) if anchored in entries: position = entries.index(anchored) + 1 entries.insert(position, moved) - matcher = difflib.SequenceMatcher(None, [entry[2] for entry in before], [entry[2] for entry in after], autojunk=False) + matcher = difflib.SequenceMatcher(None, [entry[1:3] for entry in before], [entry[1:3] for entry in after], autojunk=False) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "equal": continue diff --git a/tools/import_audit.py b/tools/import_audit.py index f0fd82f2d2..0eb4e52391 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -5,6 +5,7 @@ import re import struct import subprocess import sys +import tempfile from collections import defaultdict from pathlib import Path @@ -157,6 +158,7 @@ def elf_exports(path, data): continue if entry != 24 or size % 24 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic symbol table (entry size {entry}, size {size}, string table section {link})") + take(data, offset, size, path, "dynamic symbol table") strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") indexes = [candidate for candidate in sections if candidate[1] == 18 and candidate[6] == section_index] @@ -342,17 +344,30 @@ def source_stubs(source): if not Path(source).is_dir(): raise AuditError(f"{source}: not a directory") stubs = {} + shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in library.rglob("*.cpp"): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) + if "shared_sources" in group: + shared[library.name] = group["shared_sources"] done = {nid_of(name, aliases) for name in group["done_names"]} for name in group["todo_names"]: nid = nid_of(name, aliases) if nid not in done: stubs.setdefault((library.name + ".prx", nid), name) + for library, owner in shared.items(): + chain = [library] + while owner in shared: + if owner in chain: + raise AuditError(f"{source}: shared source cycle: {' -> '.join(chain + [owner])}") + chain.append(owner) + owner = shared[owner] + for (provider, nid), name in list(stubs.items()): + if provider == owner + ".prx": + stubs[(library + ".prx", nid)] = name return stubs @@ -448,6 +463,23 @@ def attach_names(records, names): record["name_verified"] = nid_names.compute_nid(name) == record["nid"] +def write_json(path, result): + if path.is_dir(): + raise AuditError(f"{path}: JSON output is a directory") + destination = path.resolve() + with tempfile.TemporaryDirectory(dir=destination.parent, prefix=destination.name + ".", suffix=".tmp") as directory: + temporary = Path(directory) / destination.name + try: + temporary.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + if destination.exists(): + temporary.chmod(destination.stat().st_mode & 0o7777) + temporary.replace(destination) + finally: + if temporary.exists(): + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) + + def git_stamp(source=ROOT): try: commit = subprocess.run(["git", "-C", str(source), "rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip() @@ -544,7 +576,7 @@ def main(argv=None): "library": entry["stop"]["library"] if entry["stop"] else None, "name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops] result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "stops": json_stops, "imports": records} - args.json.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + write_json(args.json, result) except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) return 2 diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 582f6bb071..4a1a4b4720 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -45,7 +45,24 @@ def open_prs(base): query = urlencode({"state": "open", "base": base, "per_page": 100}) out = gh("api", "--paginate", f"repos/{REPO}/pulls?{query}") prs = {} - for pr in json.loads(out): + decoder = json.JSONDecoder() + offset = 0 + page_count = 0 + pull_requests = [] + while offset < len(out): + while offset < len(out) and out[offset].isspace(): + offset += 1 + if offset == len(out): + break + page, offset = decoder.raw_decode(out, offset) + if not isinstance(page, list): + raise ValueError("gh api --paginate returned a non-array page") + page_count += 1 + pull_requests.extend(page) + if page_count == 0: + raise ValueError("gh api --paginate returned no JSON pages") + + for pr in pull_requests: prs[pr["number"]] = { "ref": f"refs/pr/{pr['number']}", "sha": pr["head"]["sha"], @@ -127,7 +144,7 @@ def clashes(a, b, path): def conflicts(a, b): if not (a["tree"] and b["tree"] and a["files"] & b["files"]): return {} - files = merge("--merge-base=refs/pr/base", a["tree"], b["tree"])[1] + files = merge(a["tree"], b["tree"])[1] return {path: [] if path in a["added"] else clashes(a, b, path) for path in files} diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py new file mode 100644 index 0000000000..ed4c2e5562 --- /dev/null +++ b/tools/prepare_release_notes.py @@ -0,0 +1,40 @@ +import argparse +import json +import re +from pathlib import Path + +BODY_LIMIT = 125000 + + +def release_body(notes): + if len(notes) <= BODY_LIMIT: + return notes + return re.sub( + r"^## What's Changed[^\S\r\n]*\r?\n.*?(?=^#{1,2} |^\*\*Full Changelog\*\*:|\Z)", + "", + notes, + count=1, + flags=re.MULTILINE | re.DOTALL, + ) + + +def prepare_notes(source, body, full): + notes = json.loads(source.read_text(encoding="utf-8"))["body"] + if not isinstance(notes, str): + raise ValueError("Generated release notes body must be a string") + full.parent.mkdir(parents=True, exist_ok=True) + full.write_text(notes, encoding="utf-8", newline="") + body.write_text(release_body(notes), encoding="utf-8", newline="") + + +def main(): + parser = argparse.ArgumentParser(description="Prepare size-safe GitHub release notes") + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--body", type=Path, required=True) + parser.add_argument("--full", type=Path, required=True) + args = parser.parse_args() + prepare_notes(args.input, args.body, args.full) + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py new file mode 100644 index 0000000000..b691d60d45 --- /dev/null +++ b/tools/tests/test_check_compile.py @@ -0,0 +1,202 @@ +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_compile + +TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" + + +class CheckCompileTests(unittest.TestCase): + def setUp(self): + holder = tempfile.TemporaryDirectory() + self.addCleanup(holder.cleanup) + self.root = Path(holder.name).resolve() + subprocess.run(["git", "init", "-q", str(self.root)], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.email", "tests@example.com"], check=True) + subprocess.run(["git", "-C", str(self.root), "config", "user.name", "tests"], check=True) + self.build = self.root / "build" + self.build.mkdir() + + def tool(self, *arguments, env=None): + environment = dict(os.environ) + environment.update(env or {}) + return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, + capture_output=True, text=True, env=environment) + + def source(self, relative, text="int x;\n"): + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def compiler(self, body, name="compiler.py"): + script = self.root / name + script.write_text(body, encoding="utf-8") + return script + + def entry(self, relative, command): + return {"directory": str(self.build), "command": command, "file": str(self.root / relative)} + + def manifest(self, entries): + (self.build / "compile_commands.json").write_text(json.dumps(entries), encoding="utf-8") + + def failing(self, relative, message=None): + source = self.source(relative) + message = message if message is not None else f"{relative}:1:1: error: no\n" + script = self.compiler("import sys\nsys.stderr.write(%r)\nsys.exit(1)\n" % message) + self.manifest([self.entry(relative, f'"{sys.executable}" "{script}" "{source}"')]) + return source + + def test_a_translation_unit_that_does_not_compile_fails_with_the_compiler_output(self): + self.failing("unit.cpp") + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + + def test_a_translation_unit_that_compiles_passes(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + def test_the_command_is_rewritten_to_syntax_only_without_object_or_dependency_files(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" -c -o unit.o ' + f'-MD -MT unit.o -MF unit.o.d "{source}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_nothing_checked_is_a_failure(self): + self.manifest([]) + result = self.tool("orphan.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + self.assertIn("no translation unit to check in", result.stderr) + + def test_a_translation_unit_outside_the_build_is_reported_but_the_others_decide(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("unit.cpp", "orphan.cpp") + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + self.assertIn("orphan.cpp: no compile command in", result.stderr) + + def test_an_absolute_path_names_the_same_translation_unit(self): + source = self.failing("unit.cpp") + result = self.tool(str(source)) + self.assertEqual(result.returncode, 1) + self.assertIn("unit.cpp:1:1: error: no", result.stderr) + + def test_a_relative_source_argument_in_the_command_is_replaced(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + relative = os.path.relpath(source, self.build) + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" "{relative}"')]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + + def test_a_build_directory_that_is_not_a_child_of_the_checkout_is_matched(self): + self.build = self.root / "out" / "build" + self.build.mkdir(parents=True) + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + self.manifest([self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"')]) + result = self.tool("--build", str(self.build), "unit.cpp") + self.assertEqual(result.returncode, 0) + 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) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("caf\u00e9.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("error: no", result.stderr) + + def test_a_longer_path_does_not_shadow_the_translation_unit(self): + source = self.source("src/foo.cpp") + other = self.source("plugins/x/src/foo.cpp") + passes = self.compiler("import sys\nsys.exit(0)\n", name="passes.py") + fails = self.compiler( + "import sys\nsys.stderr.write('src/foo.cpp:1:1: error: no\\n')\nsys.exit(1)\n", name="fails.py") + self.manifest([ + self.entry("plugins/x/src/foo.cpp", f'"{sys.executable}" "{passes}" "{other}"'), + self.entry("src/foo.cpp", f'"{sys.executable}" "{fails}" "{source}"'), + ]) + result = self.tool("src/foo.cpp") + self.assertEqual(result.returncode, 1) + self.assertIn("src/foo.cpp:1:1: error: no", result.stderr) + + def test_a_new_untracked_translation_unit_is_checked(self): + self.source("base.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "base.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.failing("brandnew.cpp") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("brandnew.cpp:1:1: error: no", result.stderr) + + def test_a_missing_build_directory_is_rejected(self): + result = self.tool("--build", "nowhere", "unit.cpp") + self.assertEqual(result.returncode, 2) + self.assertIn("not a directory", result.stderr) + + def test_the_changed_translation_units_are_taken_from_the_base(self): + self.failing("changed.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "changed.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.source("changed.cpp", "int y;\n") + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 1) + self.assertIn("changed.cpp:1:1: error: no", result.stderr) + + def test_no_changed_translation_unit_is_not_a_failure(self): + self.source("clean.cpp") + subprocess.run(["git", "-C", str(self.root), "add", "clean.cpp"], check=True) + subprocess.run(["git", "-C", str(self.root), "commit", "-qm", "add"], check=True) + self.manifest([]) + result = self.tool("--base", "HEAD") + self.assertEqual(result.returncode, 0) + self.assertIn("no changed translation unit", result.stdout) + + def test_the_commands_come_from_ninja_when_the_manifest_is_absent(self): + source = self.source("unit.cpp") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{source}"') + 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") + if os.name == "nt": + (bin / "ninja.cmd").write_text(f'@"{sys.executable}" "{stub}"\n', encoding="utf-8") + 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']}"}) + self.assertEqual(result.returncode, 0) + self.assertIn("1 translation units compile", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 4d3e83c0bc..25e0b1f871 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -6,6 +6,7 @@ import sys import tempfile import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output @@ -39,6 +40,18 @@ class FunctionBodyTests(unittest.TestCase): source = 'int APS5_VABI sceExample() {\nuse("// }", "/* { */");\nreturn 0;\n}' self.assertEqual(function_body(source.splitlines(), 0), 'use("","");return0;') + def test_raw_literal_braces_and_comments_preserve_following_statements(self): + source = 'int APS5_VABI sceExample() {\nuse(R"json({"brace":"}","comment":"/* hidden */"})json");\nvalidate();\nreturn 0;\n}' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'use("");validate();return0;') + self.assertIsNone(STUB_BODY.fullmatch(body)) + + def test_raw_literal_return_keeps_a_literal_sentinel(self): + source = 'const char* APS5_VABI sceExample() { return R"json({"url":"https://example.test"})json"; }' + body = function_body(source.splitlines(), 0) + self.assertEqual(body, 'return"";') + self.assertIsNone(STUB_BODY.fullmatch(body)) + def test_apostrophes_in_comments_do_not_open_character_literals(self): source = "int APS5_VABI sceExample() {\n// don't fail here\nvalidate();\n// it's fine\nreturn 0;\n}" body = function_body(source.splitlines(), 0) @@ -58,6 +71,59 @@ class FunctionBodyTests(unittest.TestCase): self.assertIsNone(function_body(source.splitlines(), 0)) +class RawLiteralCommentTests(unittest.TestCase): + path = "core/example.cpp" + + def findings(self, source, added): + check = Check.__new__(Check) + check.tree = set() + check.status = {} + check.findings = [] + lines = source.split("\n") + check.added = {self.path: [(number, lines[number - 1]) for number in added]} + with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): + check.code() + return check.findings + + def test_isolated_checker_loads_its_trusted_sibling_from_another_directory(self): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + Path(directory, "nid_names.py").write_text('raise RuntimeError("untrusted current directory")\n', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(checker), "--help"], cwd=directory, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("check a branch", result.stdout) + + def test_raw_url_does_not_create_a_comment(self): + source = 'static const char* Metadata = R"json({"url":"https://example.test"})json";' + self.assertEqual(self.findings(source, [1]), []) + + def test_blank_added_cpp_lines_preserve_positions(self): + for source, added in (("\n", [1]), ("int value;\n\n", [2])): + with self.subTest(source=source): + self.assertEqual(self.findings(source, added), []) + + def test_unicode_separators_in_literals_keep_physical_lines(self): + for separator in ("\x85", "\u2028", "\u2029"): + with self.subTest(separator=separator): + source = f'static const char* Metadata = R"text(https://example.test/{separator}more)text";\nint value; // pending\n' + self.assertEqual(self.findings(source, [1, 2]), [("comment", self.path, 2, "")]) + + def test_added_line_inside_an_existing_multiline_raw_literal_is_not_a_comment(self): + source = 'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text";\n' + self.assertEqual(self.findings(source, [2]), []) + + def test_actual_comment_after_a_raw_literal_retains_its_line(self): + for comment in ("// pending implementation", "/* pending implementation */"): + with self.subTest(comment=comment): + source = f'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text"; {comment}\n' + self.assertEqual(self.findings(source, [2, 3]), [("comment", self.path, 3, "")]) + + def test_quotes_and_raw_looking_text_do_not_hide_actual_comments(self): + for comment in ('// " R"text(https://example.test)text"', '/* " R"text(https://example.test)text" */'): + with self.subTest(comment=comment): + self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) + + class SilentStubTests(unittest.TestCase): def test_commented_stub_is_reported_when_debt_change_allows_comments(self): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" @@ -245,14 +311,68 @@ class SilentStubSectionTests(unittest.TestCase): self.assertNotIn("[debt-file]", result.stdout + result.stderr) +class TestFixtureExportTests(unittest.TestCase): + def check(self, path, text): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + def write(relative, content): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + + git("init", "-q") + write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") + git("add", ".") + git("commit", "-qm", "feat: add libraries") + write(path, text) + git("add", ".") + git("commit", "-qm", "feat: add source") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=env, timeout=20, + ) + + def test_mock_in_tests_directory_is_not_an_export(self): + result = self.check("core/libs/prx/libSceAudioOut/tests/Shutdown.cpp", + 'extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime() { return 0; }\n') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_named_like_a_test_fixture_is_not_a_duplicate(self): + result = self.check("core/libs/prx/libSceAjm/src/Port.cpp", "int APS5_VABI Process(int value) { return value + 1; }\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_export_in_another_library_is_still_a_duplicate(self): + result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) + self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + + class PullRequestTemplateTests(unittest.TestCase): - def findings(self, line): + def body_findings(self, body): check = Check.__new__(Check) check.findings = [] - body = f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n" check.pull_request("fix: x", body) return check.findings + def findings(self, line): + return self.body_findings(f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n") + def test_unchecked_items_with_any_bullet_are_reported(self): for bullet in ("-", "*", "+"): with self.subTest(bullet=bullet): @@ -261,6 +381,14 @@ class PullRequestTemplateTests(unittest.TestCase): def test_checked_item_is_accepted(self): self.assertEqual(self.findings("- [x] all"), []) + def test_subsections_stay_inside_their_section(self): + body = "### What\nx\n### Tested\n#### Linux\nctest\n#### Windows\nctest\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), []) + + def test_unchecked_item_after_a_checklist_subsection_is_reported(self): + body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n#### Later\n- [ ] all\n" + self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "unchecked: all")]) + class SpacedPathTests(unittest.TestCase): def test_comment_in_path_with_space_is_reported(self): @@ -370,6 +498,23 @@ class RepositoryChecksTests(unittest.TestCase): check.code() self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + def test_unicode_raw_literals_keep_git_patch_and_grep_records_intact(self): + literals = 'R"text(foo\u2028+tail\u2029+tail)text"' + fixtures = ( + ("core/example.cpp", f"static const char* Metadata = {literals};\n"), + ("core/libs/prx/libSceExample/Export.cpp", f"const char* APS5_VABI sceGetLabel() {{ return {literals}; }}\n"), + ) + for name, definition in fixtures: + with self.subTest(path=name): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(definition + "int value; // pending\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + self.assertEqual([number for number, _ in check.added[name]], [1, 2]) + check.code() + self.assertEqual(check.findings, [("comment", name, 2, "")]) + def test_checks_from_a_subdirectory_see_the_whole_repository(self): definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" first = self.root / "core/libs/prx/libSceA/Export.cpp" diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index ce1b342961..c2a4c33b79 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -34,6 +34,76 @@ OTHER = [ class CheckSkipsTests(unittest.TestCase): + def test_missing_high_refresh_display_is_expected_in_both_jobs(self): + reason = "current display does not provide 120 Hz" + reports = ( + f'skipped, {reason}', + f'', + f'{reason}', + f'skipped, {reason}', + f'\n\n{reason}\n' + f'\n\nskipped, {reason}\n\n' + f'\nskipped, {reason}\n', + f'\n\n{reason}\n\n{reason}\n', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + 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("1 test(s) skipped, all expected", result.stdout) + + def test_high_refresh_reason_cannot_hide_additional_diagnostics(self): + reason = "current display does not provide 120 Hz" + known = f"skipped, {reason}" + extra = "unexpected failure detail" + reports = ( + f'{known}\n{extra}', + f'{extra}\n{known}', + f'{known}\n{extra}', + f'{known}{extra}', + f'{extra}{known}', + f'{extra}', + f'{reason}{extra}', + f'{extra}', + f'{known}', + f'{reason}\n{extra}', + f'{known}\n{known}\n{extra}', + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for report in reports: + junit.write_text(f'{report}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, report=report): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + + def test_unknown_display_reasons_stay_unexpected_in_both_jobs(self): + reasons = ( + "current display is broken", + "current display does not provide 120 Hz because the test is flaky", + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + for reason in reasons: + junit.write_text(f'skipped, {reason}', encoding="utf-8") + for job in ("linux", "windows"): + with self.subTest(job=job, reason=reason): + result = subprocess.run( + [sys.executable, "-I", str(TOOL), str(junit), "--job", job], + capture_output=True, text=True) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("error: guest_video_out_high_frame_rate skipped", result.stdout) + def test_disabled_tests_fail_in_both_jobs_including_agc(self): xml = '' with tempfile.TemporaryDirectory() as directory: diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 1b5c9acc3e..9bdceffa98 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -110,6 +110,27 @@ class DebtSplitTests(unittest.TestCase): self.assertFalse((self.root / "docs/dev/debt/libSceC.md").exists()) self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_moves_unchanged_entry_to_functional(self): + result = self.rebase([A_STUB, B_STUB], [C_STUB, A_FUNCTIONAL, SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + self.git("rebase", "--continue") + self.assertEqual(self.read("docs/dev/debt/libSceC.md"), + "# libSceC technical debt\n\n### Functional\n\n" + C_STUB.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceC.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + + def test_resolve_moves_unchanged_entry_to_silent_stubs(self): + result = self.rebase([A_STUB, B_STUB, C_STUB, A_FUNCTIONAL], [SHADER, SHARED]) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + 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" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceA.md"]) + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_resolve_refuses_changes_to_lines_that_stay_in_the_index(self): result = self.rebase([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED + " too"]) self.assertNotEqual(result.returncode, 0) @@ -136,6 +157,18 @@ class DebtSplitTests(unittest.TestCase): self.assertNotEqual(result.returncode, 0) self.assertIn("not in a three-way conflict", result.stderr) + def test_resolve_preserves_entries_inserted_before_the_first_existing_entry(self): + first = "- [libSceA](../../core/libs/prx/libSceA/First.cpp) - `sceAFirst` returns 0" + second = "- [libSceA](../../core/libs/prx/libSceA/Second.cpp) - `sceASecond` returns 0" + result = self.rebase([first, second, A_STUB, B_STUB, C_STUB], [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" + + "\n".join(entry.replace("../../", "../../../") for entry in (first, second, A_STUB)) + + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") + self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 1e84d84ea6..5fc16352c7 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -2,7 +2,9 @@ import contextlib import io import json import os +import stat import struct +import subprocess import sys import tempfile import unittest @@ -102,6 +104,13 @@ def exporting(*names): return build_elf([(name, 1, GLOBAL_FUNCTION) for name in names]) +def with_elf_symbol_table_range(image, offset, size): + image = bytearray(image) + section_offset = struct.unpack_from("