diff --git a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml new file mode 100644 index 0000000000..afb5f3d3d3 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml @@ -0,0 +1,81 @@ +title: "Game name (PPSA00000)" +body: + - type: markdown + attributes: + value: | + One thread per title, for every platform: search this category for the title ID first. If a thread exists, reply there, also when your OS or GPU differs from the one it reports. + Name the discussion `Game name (title ID)`, for example `DOOM Shareware (PPSA99666)`, without the platform. + Bug reports and implementation work belong in issues and pull requests; link them here as the blocker. + Per-game progress is also tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), built from public GitHub data. Only share material you have the right to distribute. + - type: checkboxes + id: searched + attributes: + label: Existing thread + options: + - label: I searched this category for the title ID and no thread exists for this title on any platform. + required: true + - type: input + id: title + attributes: + label: Title + description: Name and title ID from `sce_sys/param.json`. + placeholder: DOOM Shareware (PPSA99666) + validations: + required: true + - type: markdown + attributes: + value: | + ### First report + The fields below describe one run. Other systems and content versions are added to the thread as replies. + - type: input + id: version + attributes: + label: Content version tested + description: The content version from `sce_sys/param.json`. + placeholder: 01.001.001 + validations: + required: true + - type: input + id: commit + attributes: + label: AnyPS5 commit + description: The `main` commit that was built, or the release that was used, and any pull requests merged on top. + validations: + required: true + - type: input + id: system + attributes: + label: System tested + description: OS, GPU and driver version, and the compiler if you built it yourself. + placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08) + validations: + required: true + - type: dropdown + id: reached + attributes: + label: Furthest point + options: + - Does not convert + - Stops before the first frame + - Logos or loading screen + - Menu + - In game + validations: + required: true + - type: textarea + id: seen + attributes: + label: What is shown + description: The last screen that was seen, the frame rate there, and what works or is missing (picture, audio, input). + validations: + required: true + - type: textarea + id: blocker + attributes: + label: Current blocker + description: The line where it stops, the issue that tracks it, and the open pull requests and local changes the title still needs. + - type: textarea + id: steps + attributes: + label: How it was run + description: Commands and environment variables, and what the title needed beyond the usage guide. diff --git a/.github/ISSUE_TEMPLATE/performance.yml b/.github/ISSUE_TEMPLATE/performance.yml new file mode 100644 index 0000000000..417fc4be2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.yml @@ -0,0 +1,65 @@ +name: Performance report +description: A slow scene or a change in speed, measured with a performance trace so that reports can be compared. +title: "perf: " +body: + - type: markdown + attributes: + value: | + Capture the scene with `APS5_TRACE_FRAMES=:` (presents; `APS5_TRACE_SECONDS=:` also works), as described under [Performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace). It writes `aps5-trace--.json.gz` and appends one line to `aps5-summary.jsonl`. + + - Run the scene once and restart the title, so the shader and pipeline caches are warm. + - Use the same scene, window, resolution and settings for every capture you compare. + - Set no other `APS5_TRACE_*` or `APS5_PROFILE_*` switch and do not redirect the log; they cost time. + - Capture the base and the change on the same machine in the same session. + + A trace holds timings, scope and thread names and counts, no game data. The summary line names the GPU, driver and OS. + - type: input + id: scene + attributes: + label: Title and scene + description: Title ID and what the window covers (menu, level, position, cutscene). + placeholder: PPSA01325, main menu after the intro + validations: + required: true + - type: textarea + id: observed + attributes: + label: What is slow or what changed + description: What you see (frame time, stutter, a phase that grew) and, for a comparison, the two commits. + validations: + required: true + - type: textarea + id: summary + attributes: + label: Summary lines + description: The line of aps5-summary.jsonl for each capture. It holds the commit, the APS5_* switches, GPU, driver, OS, resolution and frame times. + render: json + validations: + required: true + - type: textarea + id: trace + attributes: + label: Traces + description: Attach each aps5-trace--.json.gz (drag it into this field). They open in ui.perfetto.dev. + validations: + required: true + - type: textarea + id: digest + attributes: + label: Digest + description: The output of `python3 tools/trace_digest.py ` (with `--stacks .collapsed.txt` if you sampled the worker). + - type: textarea + id: stacks + attributes: + label: Worker stacks + description: Optional. The collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt), attached. It opens in speedscope.app. + - type: checkboxes + id: protocol + attributes: + label: Capture + options: + - label: The caches were warm (the scene ran once and the title was restarted) + required: true + - label: No other APS5_TRACE_* or APS5_PROFILE_* switch was set and the log was not redirected + required: true + - label: Captures compared here come from the same machine, scene and settings diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bb38de6a8a..991703ac12 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@ ### What ### Tested - + ### Checklist @@ -14,4 +14,5 @@ - [ ] Generic behaviour, not specific to one title - [ ] Depends on: - [ ] Unblocks: +- [ ] GPUs tested: - [ ] AI-assisted: diff --git a/.github/workflows/area-labels.yml b/.github/workflows/area-labels.yml index e64aaef4bf..bd3d6a370d 100644 --- a/.github/workflows/area-labels.yml +++ b/.github/workflows/area-labels.yml @@ -14,18 +14,34 @@ jobs: contents: read pull-requests: write steps: + - uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/area-state.json + key: area-labels-${{ github.run_id }} + restore-keys: area-labels- - id: prs env: GH_TOKEN: ${{ github.token }} run: | + state=$RUNNER_TEMP/area-state.json + [ -s "$state" ] || echo '{}' > "$state" + config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha) + gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number,headRefOid \ + --jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json" + jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo" + jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \ + 'with_entries(select(.key | in($open[0]))) + ($open[0] | with_entries(select(.key | IN($todo | split("\n")[]))))' "$state" > "$RUNNER_TEMP/area-next.json" { echo 'numbers<> "$GITHUB_OUTPUT" - - if: steps.prs.outputs.numbers != '' - uses: actions/checkout@v7 - if: steps.prs.outputs.numbers != '' uses: actions/labeler@v7 with: pr-number: ${{ steps.prs.outputs.numbers }} + - run: mv "$RUNNER_TEMP/area-next.json" "$RUNNER_TEMP/area-state.json" + - uses: actions/cache/save@v6 + with: + path: ${{ runner.temp }}/area-state.json + key: area-labels-${{ github.run_id }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c32d159239..4409a95534 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,11 @@ on: description: Pull request number to build required: true type: string + linux: + description: Also build and test on Linux + required: false + default: true + type: boolean permissions: contents: read @@ -103,7 +108,7 @@ jobs: } 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) => manual || files.length >= 3000 || + const touches = (...prefixes) => files.length >= 3000 || 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/'; @@ -214,7 +219,7 @@ jobs: linux: needs: eligibility - if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} + if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }} runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -239,8 +244,17 @@ jobs: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - - name: Install lavapipe for the GPU execution tests and the audio 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 + - 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 + - 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" + - uses: actions/cache/restore@v6 + with: + path: .mesa-shader-cache + key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }} + restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}- - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build @@ -250,6 +264,12 @@ jobs: env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx + MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache + - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }} + uses: actions/cache/save@v6 + with: + path: .mesa-shader-cache + key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }} - run: python3 tools/check_skips.py build/ctest.xml - name: Report tests slower than 30 s if: ${{ !cancelled() }} @@ -304,7 +324,7 @@ jobs: save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - shell: cmd - run: cmake -S . -B build %CONFIGURE% + run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" - name: Check that every test executable is registered with ctest shell: cmd run: python tools/check_test_registration.py build diff --git a/.github/workflows/conflict-label.yml b/.github/workflows/conflict-label.yml index d6183c9eb8..0231a79c0d 100644 --- a/.github/workflows/conflict-label.yml +++ b/.github/workflows/conflict-label.yml @@ -9,6 +9,10 @@ on: permissions: {} +concurrency: + group: conflict-label + cancel-in-progress: true + jobs: label: runs-on: ubuntu-latest diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index f7090fa246..5fa69ccd0b 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -35,3 +35,4 @@ jobs: label prx c5def5 'Other PRX libraries' label build bfd4f2 'CMake, submodules, 3rdparty' label docs 0075ca 'Documentation' + label stale ededed 'Conflicting with main and no activity for 7 days' diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000000..d789d4de62 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,55 @@ +name: Pages + +on: + push: + branches: [main] + workflow_dispatch: + release: + types: [published] + workflow_run: + workflows: [Release] + types: [completed] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + pages: + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + - name: Build web converter + shell: bash + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk" + "$RUNNER_TEMP/emsdk/emsdk" install 6.0.3 + "$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3 + source "$RUNNER_TEMP/emsdk/emsdk_env.sh" + emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release + cmake --build build-wasm --target relinker_web --parallel 2 + - name: Test web converter + run: python3 tools/test_web.py --build build-wasm + - name: Download latest release libraries + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 tools/build_web.py --build build-wasm --libraries + - run: python3 tools/progress.py build-wasm/web + - run: python3 tools/progress_history.py build-wasm/web + - uses: actions/upload-pages-artifact@v5 + with: + path: build-wasm/web + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/pr-overlap.yml b/.github/workflows/pr-overlap.yml index 1e181a5f65..f6d437eebd 100644 --- a/.github/workflows/pr-overlap.yml +++ b/.github/workflows/pr-overlap.yml @@ -22,15 +22,30 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 + - uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/overlap-state.json + key: pr-overlap-${{ github.run_id }} + restore-keys: pr-overlap- - env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | out=$RUNNER_TEMP/overlap + state=$RUNNER_TEMP/overlap-state.json python3 tools/pr_overlap.py --base "${{ github.event.repository.default_branch }}" --out "$out" + [ -s "$state" ] || echo '{}' > "$state" + record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; } + shopt -s nullglob marker='' + prev= for file in "$out"/*.md; do + [ -z "$prev" ] || record "$prev" "$hash" pr=$(basename "$file" .md) + hash=$(sha256sum "$file" | cut -d' ' -f1) + prev= + [ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue + prev=$pr comment=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \ --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1) id=$(jq -r '.[0] // empty' <<< "${comment:-null}") @@ -45,3 +60,11 @@ jobs: gh api -X DELETE "repos/$GH_REPO/issues/comments/$id" fi done + [ -z "$prev" ] || record "$prev" "$hash" + reports=$(for file in "$out"/*.md; do basename "$file" .md; done | jq -R . | jq -sc .) + jq -c --argjson keep "$reports" 'with_entries(select(.key | IN($keep[])))' "$state" > "$state.new" && mv "$state.new" "$state" + - if: always() + uses: actions/cache/save@v6 + with: + path: ${{ runner.temp }}/overlap-state.json + key: pr-overlap-${{ github.run_id }} diff --git a/.github/workflows/progress.yml b/.github/workflows/progress.yml deleted file mode 100644 index 93e9f06000..0000000000 --- a/.github/workflows/progress.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Progress - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - progress: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v7 - - run: python3 tools/progress.py out - - uses: actions/upload-pages-artifact@v5 - with: - path: out - - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f54726afa7..cbab5a8f97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 + 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: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml new file mode 100644 index 0000000000..4eee09a40e --- /dev/null +++ b/.github/workflows/stale-conflicts.yml @@ -0,0 +1,28 @@ +name: Stale conflicts + +on: + schedule: + - cron: '30 3 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + warn: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v11 + with: + only-pr-labels: needs rebase + days-before-issue-stale: -1 + days-before-issue-close: -1 + days-before-pr-stale: 7 + days-before-pr-close: -1 + stale-pr-label: stale + stale-pr-message: 'This pull request has conflicted with `main` and had no activity for 7 days. Rebase it on current `main` so it can be reviewed and merged. It will not be closed automatically.' + remove-pr-stale-when-updated: true + exempt-draft-pr: true + operations-per-run: 200 diff --git a/.gitmodules b/.gitmodules index fc2e48631c..ddbcc22956 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,12 @@ [submodule "3rdparty/stb"] path = 3rdparty/stb url = https://github.com/nothings/stb.git +[submodule "3rdparty/tracy"] + path = 3rdparty/tracy + url = https://github.com/wolfpld/tracy.git [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git +[submodule "3rdparty/libusb-cmake"] + path = 3rdparty/libusb-cmake + url = https://github.com/libusb/libusb-cmake.git diff --git a/3rdparty/libusb-cmake b/3rdparty/libusb-cmake new file mode 160000 index 0000000000..bea6567b63 --- /dev/null +++ b/3rdparty/libusb-cmake @@ -0,0 +1 @@ +Subproject commit bea6567b63796ab27c1c33257d230284fb2d8316 diff --git a/3rdparty/tracy b/3rdparty/tracy new file mode 160000 index 0000000000..30997d5ca6 --- /dev/null +++ b/3rdparty/tracy @@ -0,0 +1 @@ +Subproject commit 30997d5ca6bb632cc10807a1da8a6d3de0aeeb3c diff --git a/CMakeLists.txt b/CMakeLists.txt index c0b526313c..3eb8dd5333 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,19 @@ endfunction() option(BUILD_TESTING "Build and register relinker tests" OFF) option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF) +option(ANYPS5_BUILD_WASM "Build the browser relinker" OFF) +if(ANYPS5_BUILD_WASM) + if(NOT EMSCRIPTEN) + message(FATAL_ERROR "ANYPS5_BUILD_WASM requires emcmake and Emscripten") + endif() + if(BUILD_TESTING) + message(FATAL_ERROR "Use the native build for C++ tests and the browser tests for WASM") + endif() + set(ANYPS5_RELINKER_CLI OFF CACHE BOOL "Build the native relinker command line" FORCE) + add_subdirectory(core/relinker) + return() +endif() + if(BUILD_TESTING) enable_testing() if(MINGW) @@ -26,6 +39,7 @@ endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) +option(APS5_ENABLE_TRACY "Build the Tracy profiler client and report performance timer scopes as Tracy zones" OFF) option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) add_compile_definitions(APS5_ENABLE_TIMING_LOG=$) include(cmake/SystemDeps.cmake) @@ -62,12 +76,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_dependencies.py) add_test(NAME pr_overlap_tools COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap.py) + add_test(NAME progress_history COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_history.py) add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py) add_test(NAME progress_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py) add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py) + add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py) + add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py) add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} @@ -84,12 +104,16 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py) + add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py) add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py) add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py) + add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} @@ -97,6 +121,12 @@ if(BUILD_TESTING) add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py) set_tests_properties(pad_input PROPERTIES TIMEOUT 20) + add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py) + add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} + ${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) endif() endif() @@ -126,6 +156,12 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_directory_property(ANYPS5_SDL_X11_XRANDR DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_X11_XRANDR) + if(NOT ANYPS5_SDL_X11_XRANDR) + message(FATAL_ERROR "SDL has no XRandR support, so X11 display refresh rates cannot be queried. " + "Install libxrandr-dev (Debian, Ubuntu) or libXrandr-devel (Fedora), " + "enable SDL_X11_XRANDR, then rerun configuration.") + endif() get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE) get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO) get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA) @@ -144,6 +180,10 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(ffmpeg INTERFACE LINKER:--exclude-libs,ALL) endif() +if(APS5_ENABLE_TRACY) + include(cmake/Tracy.cmake) +endif() + set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE) @@ -160,6 +200,14 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) endif() endif() +set(LIBUSB_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(LIBUSB_INSTALL_TARGETS OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(LIBUSB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE) +add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL) +set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) + add_subdirectory(core/shader/recompiler) add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/Decoder/Png) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f65e0c405..b83c070c09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,11 +53,14 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - Open pull requests per author are limited by how many of theirs are merged into `main`: 1 with none merged, 20 with at least 1, no limit from 20. A pull request opened or reopened over the limit is closed automatically; reopen it once another one is merged or closed. - Before starting, check that no open pull request already implements the same functions. - Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine. +- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed. - If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). - If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. - Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced. - The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`. - Run the tests before opening the pull request and describe what was tested (OS, title or homebrew). +- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form. +- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging. - Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments. - Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it. diff --git a/README.md b/README.md index 5e57cb94ff..b07e1abc67 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,33 @@ Tool for automatic executables porting to Linux and Windows. +[**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) + Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. [Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md) ## Status -[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/) +[![libraries](https://boykopovar.github.io/AnyPS5/badge-libraries.svg)](https://boykopovar.github.io/AnyPS5/progress.html) [![shaders](https://boykopovar.github.io/AnyPS5/badge-shaders.svg)](https://boykopovar.github.io/AnyPS5/progress.html) -[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/) +[![progress map](https://boykopovar.github.io/AnyPS5/progress.svg)](https://boykopovar.github.io/AnyPS5/progress.html) + +[Progress history](https://boykopovar.github.io/AnyPS5/history.html) * System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared. -[List of verified games](docs/user/COMPATIBILITY.md) - -Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz. - Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates. The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V (validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`). ## Compatibility -See the [game compatibility list](docs/user/COMPATIBILITY.md) for tested games and known issues. +Per-game progress (stage reached, open blockers, libraries and GPU coverage) is tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), a community project built from public GitHub data ([source](https://github.com/AlxcBTW/anyps5-roadmap)). Test reports go in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. ## Input mapping diff --git a/cmake/Tracy.cmake b/cmake/Tracy.cmake new file mode 100644 index 0000000000..d108607a06 --- /dev/null +++ b/cmake/Tracy.cmake @@ -0,0 +1,21 @@ +set(TRACY_ENABLE ON CACHE BOOL "" FORCE) +set(TRACY_STATIC OFF CACHE BOOL "" FORCE) +set(TRACY_ON_DEMAND ON CACHE BOOL "" FORCE) +set(TRACY_ONLY_LOCALHOST ON CACHE BOOL "" FORCE) +set(TRACY_NO_CRASH_HANDLER ON CACHE BOOL "" FORCE) +set(TRACY_NO_CODE_TRANSFER ON CACHE BOOL "" FORCE) +set(TRACY_NO_SAMPLING ON CACHE BOOL "" FORCE) +set(TRACY_NO_SYSTEM_TRACING ON CACHE BOOL "" FORCE) +set(TRACY_NO_CALLSTACK ON CACHE BOOL "" FORCE) +set(TRACY_NO_FRAME_IMAGE ON CACHE BOOL "" FORCE) +set(TRACY_NO_VERIFY ON CACHE BOOL "" FORCE) +set(CMAKE_DISABLE_FIND_PACKAGE_rocprofiler-sdk ON) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/tracy ${CMAKE_BINARY_DIR}/3rdparty/tracy EXCLUDE_FROM_ALL) + +if(MINGW) + target_sources(TracyClient PRIVATE ${CMAKE_CURRENT_LIST_DIR}/TracyThreadExit.cpp) + target_compile_definitions(TracyClient PRIVATE TRACY_DELAYED_INIT) + get_target_property(tracyInterfaceDefinitions TracyClient INTERFACE_COMPILE_DEFINITIONS) + list(FILTER tracyInterfaceDefinitions EXCLUDE REGEX "^(WINVER|_WIN32_WINNT)=") + set_target_properties(TracyClient PROPERTIES INTERFACE_COMPILE_DEFINITIONS "${tracyInterfaceDefinitions}") +endif() diff --git a/cmake/TracyThreadExit.cpp b/cmake/TracyThreadExit.cpp new file mode 100644 index 0000000000..704dce3e4f --- /dev/null +++ b/cmake/TracyThreadExit.cpp @@ -0,0 +1,3 @@ +extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) { + return 0; +} diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cfc..707f668261 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -25,7 +25,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i target_compile_features(nid_patcher PRIVATE cxx_std_20) if(MINGW) - target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++) + target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++) include(CheckCXXSourceCompiles) set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables) check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS) @@ -149,6 +149,24 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() endforeach() +if(APS5_ENABLE_TRACY) + set_target_properties(TracyClient PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR} + ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR} + ) + set(TRACY_CLIENT_COPIED ${CMAKE_CURRENT_BINARY_DIR}/TracyClient.copied) + add_custom_command( + OUTPUT ${TRACY_CLIENT_COPIED} + COMMAND ${CMAKE_COMMAND} -E make_directory ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E copy $ ${PATCHED_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${TRACY_CLIENT_COPIED} + DEPENDS TracyClient + COMMENT "Copying the Tracy client next to the patched libraries" + ) + list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED}) +endif() + add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) @@ -161,13 +179,31 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp) + target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include) + target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20) + if(MINGW) + target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests) + set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10) + find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $) add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $) + add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $) + set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20) endif() add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp) add_dependencies(patched_libraries_tests libs) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $) + endif() if(MINGW) target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) @@ -178,6 +214,10 @@ if(BUILD_TESTING) target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc) configure_windows_unwind(guest_kernel_sys_tests) add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $) + endif() if(WIN32) set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -185,12 +225,26 @@ if(BUILD_TESTING) if(APPLE) add_subdirectory(tests/write_watch) endif() + if(WIN32) + add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) + target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) + set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + 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) configure_windows_unwind(guest_offline_imports_tests) add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests) + if(Python3_Interpreter_FOUND) + add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $) + endif() if(WIN32) set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -206,6 +260,26 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp) + target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc) + configure_windows_unwind(guest_netctl_ap_dialog_tests) + add_test(NAME guest_netctl_ap_dialog COMMAND guest_netctl_ap_dialog_tests) + if(WIN32) + set_tests_properties(guest_netctl_ap_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_np_auth_tests tests/GuestNpAuth.cpp) + target_include_directories(guest_np_auth_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_auth_tests PRIVATE libSceNpAuth libc) + configure_windows_unwind(guest_np_auth_tests) + add_test(NAME guest_np_auth COMMAND guest_np_auth_tests) + if(WIN32) + set_tests_properties(guest_np_auth PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp) target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc) @@ -363,6 +437,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 29dddbc5ab..3f73232949 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -280,6 +280,12 @@ struct ModuleInfoForUnwind { std::uint64_t seg0_size; }; +struct InternalSegmentInfo { + std::uint64_t address; + std::uint64_t size; +}; +static_assert(sizeof(InternalSegmentInfo) == 0x10); + struct MallocReplace { std::uint64_t size = sizeof(MallocReplace); void* malloc_initialize = nullptr; @@ -1259,6 +1265,13 @@ struct NetCtlNatInfo { std::uint8_t opaque[128]; }; union NetCtlInfo { std::uint8_t opaque[256]; }; +struct NetCtlIfStat { + std::uint32_t device; + std::uint64_t tx_bytes; + std::uint64_t rx_bytes; + std::uint32_t reserved[8]; +}; + using NetCtlCallback = void (*)(int, void*); struct HttpEpoll; @@ -1617,6 +1630,35 @@ struct ContentExportCallbackParam { struct ContentSearchInitParam { std::size_t memory_size; }; +struct ContentSearchColumnValue { + std::uint32_t size; + std::int64_t value; +}; + +struct ContentSearchContentColumn { + std::int32_t column_type; + std::int32_t column_operator; + const ContentSearchColumnValue* value; + std::int32_t logical_connector; +}; + +struct ContentSearchContentColumnSet { + const ContentSearchContentColumn* columns; + std::int32_t columns_length; + std::int32_t logical_connector; +}; + +struct ContentSearchContentOrderByCondition { + std::int32_t column_type; + std::int32_t sort_type; +}; + +static_assert(sizeof(ContentSearchColumnValue) == 16); +static_assert(sizeof(ContentSearchContentColumn) == 24); +static_assert(offsetof(ContentSearchContentColumn, logical_connector) == 16); +static_assert(sizeof(ContentSearchContentColumnSet) == 16); +static_assert(sizeof(ContentSearchContentOrderByCondition) == 8); + struct PngEncCreateParam { std::uint32_t this_size; std::uint32_t attribute; diff --git a/core/libs/nid/include/nid/NativePath.hpp b/core/libs/nid/include/nid/NativePath.hpp new file mode 100644 index 0000000000..994be15dfa --- /dev/null +++ b/core/libs/nid/include/nid/NativePath.hpp @@ -0,0 +1,15 @@ +#ifndef NID_NATIVEPATH_HPP +#define NID_NATIVEPATH_HPP + +#include +#include + +namespace Nid::Internal { + +inline std::filesystem::path NativePath(const std::string& path) { + return std::filesystem::path(std::u8string(path.begin(), path.end())); +} + +} + +#endif diff --git a/core/libs/nid/include/nid/NidPatcherUtils.hpp b/core/libs/nid/include/nid/NidPatcherUtils.hpp index f467756124..33ecd14f8b 100644 --- a/core/libs/nid/include/nid/NidPatcherUtils.hpp +++ b/core/libs/nid/include/nid/NidPatcherUtils.hpp @@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) { template T Read(const std::vector& buf, std::size_t offset) { - if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds"); + if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds"); T v; std::memcpy(&v, buf.data() + offset, sizeof(T)); return v; @@ -69,7 +69,7 @@ T Read(const std::vector& buf, std::size_t offset) { template void Write(std::vector& buf, std::size_t offset, const T& v) { - if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds"); + if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds"); std::memcpy(buf.data() + offset, &v, sizeof(T)); } @@ -78,6 +78,7 @@ inline std::string ReadCStr(const std::vector& buf, std::size_t of std::string s; while (offset < buf.size() && buf[offset] != 0u) s.push_back(static_cast(buf[offset++])); + if (offset == buf.size()) throw std::runtime_error("unterminated cstr"); return s; } diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index 4f341bbd26..e97b8bfcb2 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -36,6 +36,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector& elf, std::size_ return layout; } +void ValidateGnuHash(const std::vector& elf, std::size_t offset, std::size_t size, std::size_t symCount) { + if (offset > elf.size() || size > elf.size() - offset || size < 16u) + throw std::runtime_error("invalid gnu.hash section range"); + const auto layout = ReadGnuHashLayout(elf, offset); + if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) || + layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount)) + throw std::runtime_error("invalid gnu.hash geometry"); + const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u + + std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u; + if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place"); +} + void ReorderDynSymForGnuHash( std::vector& elf, std::size_t dynSymOffset, @@ -204,49 +216,65 @@ void RemapDynamicNeededOffsets( } } +struct VersionNameField { + std::size_t Offset; + std::uint32_t Name; +}; + +std::vector ReadVersionNameFields(const std::vector& elf, const Elf64_Ehdr& ehdr) { + using namespace Internal; + std::vector fields; + for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { + const auto section = Read(elf, static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr)); + if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue; + if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset) + throw std::runtime_error("invalid .gnu.version_r section range"); + const auto location = [&](std::uint64_t relative, std::size_t size) { + if (relative > section.sh_size || size > section.sh_size - relative) + throw std::runtime_error(".gnu.version_r record outside section"); + return static_cast(section.sh_offset + relative); + }; + const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) { + if (step < size || step > section.sh_size - current) + throw std::runtime_error("invalid .gnu.version_r record link"); + return current + step; + }; + std::uint64_t current = 0u; + while (true) { + const auto offset = location(current, sizeof(Elf64_Verneed)); + const auto need = Read(elf, offset); + if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version"); + fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file}); + if (need.vn_cnt != 0u) { + auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed)); + for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) { + const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux)); + const auto aux = Read(elf, auxOffset); + fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name}); + if (index + 1u == need.vn_cnt) { + if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch"); + } else { + auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux)); + } + } + } + if (need.vn_next == 0u) break; + current = advance(current, need.vn_next, sizeof(Elf64_Verneed)); + } + } + return fields; +} + void RemapVerneedOffsets( std::vector& elf, const Elf64_Ehdr& ehdr, const std::vector>& oldToNewOffset ) { using namespace Internal; - - for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { - const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); - const auto shdr = Read(elf, shOffset); - if (shdr.sh_type != kShtGnuVerneed) continue; - - const std::size_t verneedOffset = static_cast(shdr.sh_offset); - std::size_t entryOffset = verneedOffset; - - while (true) { - const auto verneed = Read(elf, entryOffset); - - const auto mappedFile = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == verneed.vn_file; } - ); - if (mappedFile != oldToNewOffset.end()) - Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second); - - std::size_t auxOffset = entryOffset + verneed.vn_aux; - for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) { - const auto vernaux = Read(elf, auxOffset); - - const auto mappedName = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == vernaux.vna_name; } - ); - if (mappedName != oldToNewOffset.end()) - Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second); - - if (vernaux.vna_next == 0u) break; - auxOffset += vernaux.vna_next; - } - - if (verneed.vn_next == 0u) break; - entryOffset += verneed.vn_next; - } + for (const auto& field : ReadVersionNameFields(elf, ehdr)) { + const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(), + [&](const auto& entry) { return entry.first == field.Name; }); + if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second); } } @@ -275,35 +303,8 @@ std::vector CollectDynamicNameOffsets(const std::vector CollectVerneedNameOffsets(const std::vector& elf, const Elf64_Ehdr& ehdr) { - using namespace Internal; - std::vector offsets; - - for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { - const std::size_t shOffset = static_cast(ehdr.e_shoff) + i * sizeof(Elf64_Shdr); - const auto shdr = Read(elf, shOffset); - if (shdr.sh_type != kShtGnuVerneed) continue; - - std::size_t entryOffset = static_cast(shdr.sh_offset); - - while (true) { - const auto verneed = Read(elf, entryOffset); - offsets.push_back(verneed.vn_file); - - std::size_t auxOffset = entryOffset + verneed.vn_aux; - for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) { - const auto vernaux = Read(elf, auxOffset); - offsets.push_back(vernaux.vna_name); - - if (vernaux.vna_next == 0u) break; - auxOffset += vernaux.vna_next; - } - - if (verneed.vn_next == 0u) break; - entryOffset += verneed.vn_next; - } - } - + for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name); return offsets; } @@ -333,7 +334,6 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& if (shdr.sh_type == kShtGnuHash) { gnuHashOffset = static_cast(shdr.sh_offset); gnuHashSize = static_cast(shdr.sh_size); - gnuHashSymOffset = Read(elf, gnuHashOffset + 4u); } if (shdr.sh_type == kShtGnuVersym) versymOffset = static_cast(shdr.sh_offset); @@ -345,6 +345,10 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym); if (symCount == 0u) throw std::runtime_error(".dynsym is empty"); + if (gnuHashOffset != 0u) { + ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount); + gnuHashSymOffset = Read(elf, gnuHashOffset + 4u); + } const std::size_t dynStrSectionLink = [&]() -> std::size_t { for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) { @@ -427,7 +431,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const auto addName = [&](std::uint32_t oldOffset, std::string value) { const auto existing = std::find_if( names.begin(), names.end(), - [&](const auto& entry) { return entry.first == oldOffset; } + [&](const auto& entry) { return entry.first == oldOffset && entry.second == value; } ); if (existing == names.end()) names.emplace_back(oldOffset, std::move(value)); }; @@ -463,8 +467,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& newDynStr.push_back(0u); } - std::vector> oldToNewOffset; - for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second)); + std::vector> preservedToNewOffset; + for (const auto offset : preservedOffsets) + preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset))); if (newDynStr.size() > dynStrSize) { throw std::runtime_error( @@ -475,25 +480,15 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& newDynStr.resize(dynStrSize, 0u); std::vector newSymbolNames(symCount); - for (std::size_t i = 1u; i < symCount; ++i) { - const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym); - const auto sym = Read(elf, symOffset); - if (sym.st_name == 0u) continue; - - const auto mapped = std::find_if( - oldToNewOffset.begin(), oldToNewOffset.end(), - [&](const auto& entry) { return entry.first == sym.st_name; } - ); - if (mapped == oldToNewOffset.end()) continue; - - newSymbolNames[i] = ReadCStr(newDynStr, mapped->second); - Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second); + for (const auto& use : uses) { + newSymbolNames[use.symIndex] = use.newValue; + Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue)); } std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size()); - RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset); - RemapVerneedOffsets(elf, ehdr, oldToNewOffset); + RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset); + RemapVerneedOffsets(elf, ehdr, preservedToNewOffset); if (gnuHashOffset != 0u) { const auto bucketCount = Read(elf, gnuHashOffset); diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 9c00d6b3e4..366e918cb5 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -49,7 +50,12 @@ std::unordered_set ReadPeExports(const std::vector& b if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports"); const auto sections = optional + optionalSize; RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader)); + const auto headersSize = ReadValue(binary, optional + 60); const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t { + if (rva < headersSize && size <= headersSize - rva) { + RequireRange(binary, rva, size); + return rva; + } for (std::uint16_t index = 0; index < sectionCount; ++index) { const auto section = ReadValue(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader)); if (rva < section.VirtualAddress) continue; @@ -64,14 +70,20 @@ std::unordered_set ReadPeExports(const std::vector& b const auto table = ReadValue(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory))); if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports"); const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4); + const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2); + const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4); std::unordered_set result; for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) { + const auto ordinal = ReadValue(binary, ordinals + std::uint64_t{index} * 2); + if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal"); + if (!ReadValue(binary, functions + std::uint64_t{ordinal} * 4)) continue; const auto rva = ReadValue(binary, names + std::uint64_t{index} * 4); const auto offset = mapRva(rva, 1); auto name = ReadName(binary, offset, binary.size() - offset); mapRva(rva, name.size() + 1); if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name); } + if (result.empty()) throw std::runtime_error("reference PE has no named exports"); return result; } @@ -110,7 +122,7 @@ std::string NormalizeExportName(const std::string& name) { std::unordered_set ReadExportExclusions(const std::string& path) { if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host"); - std::ifstream input(path, std::ios::binary | std::ios::ate); + std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate); if (!input) throw std::runtime_error("cannot open export reference: " + path); const auto size = input.tellg(); if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path); diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index 2f91413fd6..c07a4e8bfa 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -9,20 +10,20 @@ namespace { std::vector _readFile(const std::string& path) { - std::ifstream f(path, std::ios::binary); + std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary); if (!f) throw std::runtime_error("cannot open: " + path); return {std::istreambuf_iterator(f), {}}; } void _writeFile(const std::string& path, const std::vector& data) { - std::ofstream f(path, std::ios::binary); + std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary); if (!f) throw std::runtime_error("cannot write: " + path); f.write(reinterpret_cast(data.data()), static_cast(data.size())); } } -int main(int argc, char* argv[]) { +int Run(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: nid_patcher [--preserve-exports ] [file2 ...]\n"; return 1; @@ -65,3 +66,27 @@ int main(int argc, char* argv[]) { return 0; } + +#ifdef _WIN32 +int wmain(int argc, wchar_t* argv[]) { + try { + std::vector values; + values.reserve(static_cast(argc)); + for (int index = 0; index < argc; ++index) { + const auto utf8 = std::filesystem::path(argv[index]).u8string(); + values.emplace_back(utf8.begin(), utf8.end()); + } + std::vector arguments; + arguments.reserve(values.size()); + for (auto& value : values) arguments.push_back(value.data()); + return Run(argc, arguments.data()); + } catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 2; + } +} +#else +int main(int argc, char* argv[]) { + return Run(argc, argv); +} +#endif diff --git a/core/libs/nid/tests/BufferValidation.cpp b/core/libs/nid/tests/BufferValidation.cpp new file mode 100644 index 0000000000..a006371d89 --- /dev/null +++ b/core/libs/nid/tests/BufferValidation.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition) { + if (!condition) throw std::runtime_error("NID buffer validation failed"); +} + +template +void RequireRejected(TAction action) { + try { action(); } + catch (const std::runtime_error&) { return; } + throw std::runtime_error("Malformed NID buffer was accepted"); +} + +} + +int main() { + try { + using namespace Nid::Internal; + const std::vector unterminated{'n', 'a', 'm', 'e'}; + RequireRejected([&] { ReadCStr(unterminated, 0); }); + RequireRejected([&] { ReadCStr(unterminated, 3); }); + const std::vector strings{'a', 0, 'b', 'c', 0}; + Require(ReadCStr(strings, 0) == "a"); + Require(ReadCStr(strings, 1).empty()); + Require(ReadCStr(strings, 2) == "bc"); + Require(ReadCStr(strings, 4).empty()); + RequireRejected([&] { ReadCStr(strings, strings.size()); }); + RequireRejected([&] { ReadCStr({}, 0); }); + + std::vector bytes(8, 0xa5); + const std::uint64_t value = 0x1020304050607080; + Write(bytes, 0, value); + Require(Read(bytes, 0) == value); + Write(bytes, 4, 0x12345678); + Require(Read(bytes, 4) == 0x12345678); + const auto before = bytes; + for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9}, + std::numeric_limits::max(), std::numeric_limits::max() - 1, + std::numeric_limits::max() - 3}) { + RequireRejected([&] { Read(bytes, offset); }); + RequireRejected([&] { Write(bytes, offset, 0); }); + Require(bytes == before); + } + RequireRejected([&] { Read(bytes, 1); }); + std::vector empty; + RequireRejected([&] { Read(empty, 0); }); + RequireRejected([&] { Write(empty, 0, 0); }); + std::cout << "NID buffer validation passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/nid/tests/test_gnu_hash.py b/core/libs/nid/tests/test_gnu_hash.py new file mode 100644 index 0000000000..3b2d27bbce --- /dev/null +++ b/core/libs/nid/tests/test_gnu_hash.py @@ -0,0 +1,75 @@ +import struct +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash + + +class GnuHashTests(unittest.TestCase): + def image(self, buckets=4, bloom=1, shift=6, symoffset=3): + image = build_image(symbols(False), False) + count = len(symbols(False)) + struct.pack_into("> shift) % 64))) + start = struct.unpack_from(" -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI ErrorDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ErrorDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SystemServiceParamGetInt(int param_id, int* value) { - (void)param_id; - (void)value; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI SystemServiceParamGetString(int param_id, char* buf, size_t buf_size) { - (void)param_id; - (void)buf; - (void)buf_size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp b/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp deleted file mode 100644 index b488eb139b..0000000000 --- a/core/libs/prx/libReactNative.Modules.Vsh/Export.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -int APS5_VABI RemotePlayGetConnectionStatus(int user_id, int* status) { - (void)user_id; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI ShareTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index fd80c9e1a7..97dfc19d91 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -61,4 +61,9 @@ int APS5_VABI sceAcm_Panner(void) { return 0; } +int APS5_VABI sceAcmBatchJobPriority(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index cfacb3c60c..0eb8d4fd17 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -5,11 +5,22 @@ #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(function); + return nullptr; +} + +} + extern "C" { std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { @@ -70,6 +81,21 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide return nullptr; } +std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { + (void)workloadIds; + (void)workloadCount; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); +} + +std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { + (void)workloadId; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); +} + +std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); +} + uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { (void)color; return Agc::Marker::Push(buf, str, __func__); diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 130df05665..0e8338c3da 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp @@ -47,6 +48,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Predication/src/Predication.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/CreateShader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsOversubscription.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsPrimPayload.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/InterpolantMapping.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/LinkShaders.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/PrimState.cpp @@ -94,6 +96,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp) +target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_semaphore_tests) +if(BUILD_TESTING) + set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests) + if(WIN32) + set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp) target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc) @@ -291,6 +306,22 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_gs_prim_payload_tests EXCLUDE_FROM_ALL + tests/GsPrimPayload.cpp + Shader/src/GsPrimPayload.cpp +) +target_include_directories(agc_gs_prim_payload_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_gs_prim_payload_tests PRIVATE libc) +configure_windows_unwind(agc_gs_prim_payload_tests) +if(BUILD_TESTING) + set_target_properties(agc_gs_prim_payload_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_gs_prim_payload_tests COMMAND agc_gs_prim_payload_tests) + if(WIN32) + set_tests_properties(agc_gs_prim_payload_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL tests/DmaData.cpp Command/src/Packet.cpp diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 7ad33c6d90..27f4ec9738 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,6 +7,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); + extern "C" { int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { @@ -16,6 +18,7 @@ int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { } int APS5_VABI sceAgcInit(std::uint32_t version) { + if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 5db03f98a4..0f739d9055 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -39,7 +39,7 @@ uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t st } uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount) return nullptr; + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; std::uint64_t mask = 0; for (std::uint32_t index = 0; index < workload_count; ++index) { const auto id = workload_ids[index]; @@ -48,7 +48,6 @@ uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t str if ((mask & bit) != 0) return nullptr; mask |= bit; } - if (!AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; auto* packet = NewPacket(buf, ActivePacketWords, __func__); packet[1] = stream_id; packet[2] = static_cast(mask); @@ -56,4 +55,10 @@ uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t str return packet; } +uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(__func__); + return nullptr; +} + } diff --git a/core/libs/prx/libSceAgc/Misc/include/Semaphore.hpp b/core/libs/prx/libSceAgc/Misc/include/Semaphore.hpp new file mode 100644 index 0000000000..834dcdd962 --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/include/Semaphore.hpp @@ -0,0 +1,4 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP +#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP + +#endif diff --git a/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp new file mode 100644 index 0000000000..7a6c802690 --- /dev/null +++ b/core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp @@ -0,0 +1,61 @@ +#include "prx/libSceAgc/Misc/include/Semaphore.hpp" +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u; +constexpr std::uint64_t semaphoreLabelBytes = 32u; +constexpr std::uint64_t semaphoreCounterBytes = 8u; + +std::mutex semaphoreMutex; +std::uint8_t* semaphoreMemory = nullptr; +std::uint64_t semaphoreBytes = 0; + +bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) { + for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { + for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) { + if (memory[label + byte] != 0) return false; + } + } + return true; +} + +} + +extern "C" { + +int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) { + const auto address = reinterpret_cast(memory); + std::lock_guard lock(semaphoreMutex); + Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown"); + Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown"); + Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown"); + Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits::max() - address, __func__, "AMM semaphore memory wraps the address space"); + auto* labels = static_cast(memory); + Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown"); + for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) { + std::memset(labels + label, 0, semaphoreCounterBytes); + } + semaphoreMemory = labels; + semaphoreBytes = sizeInBytes; + return 0; +} + +int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) { + std::lock_guard lock(semaphoreMutex); + Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown"); + Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown"); + Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown"); + *label = semaphoreMemory + static_cast(index) * semaphoreLabelBytes; + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp index 4248c29fda..4af5275789 100644 --- a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp +++ b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp @@ -25,6 +25,7 @@ constexpr std::uint32_t GE_PC_ALLOC = 0x260u; constexpr std::uint32_t SPI_SHADER_PGM_RSRC4_GS = 0x081u; constexpr std::uint32_t SPI_VS_OUT_CONFIG = 0x1B1u; +constexpr std::uint32_t SPI_SHADER_IDX_FORMAT = 0x1C2u; constexpr std::uint32_t GE_MAX_OUTPUT_PER_SUBGROUP = 0x1FFu; constexpr std::uint32_t PA_CL_VS_OUT_CNTL = 0x207u; constexpr std::uint32_t VGT_GS_ONCHIP_CNTL = 0x291u; diff --git a/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp b/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp new file mode 100644 index 0000000000..722ade98c6 --- /dev/null +++ b/core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp @@ -0,0 +1,39 @@ +#include +#include +#include +#include + +#include "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" + +namespace { + +constexpr std::uint32_t IDX0_EXPORT_FORMAT_MASK = 0xFu; +constexpr std::uint32_t SPI_SHADER_2COMP = 2u; +constexpr std::uint32_t TwoComponentPayloadBytes = 8u; + +} + +extern "C" { + +int APS5_VABI sceAgcGetGsPrimPayload(std::uint32_t* payload, const Shader* gs) { + if (payload == nullptr || gs == nullptr) { + throw std::runtime_error(std::string(__func__) + ": payload or gs is null"); + } + if (gs->num_cx_registers != 0 && gs->cx_registers == nullptr) { + throw std::runtime_error(std::string(__func__) + ": gs->cx_registers is null but num_cx_registers != 0"); + } + + std::uint32_t bytes = 0; + for (std::uint32_t i = 0; i < gs->num_cx_registers; ++i) { + const ShaderRegister& reg = gs->cx_registers[i]; + if (reg.offset == ShaderRegs::SPI_SHADER_IDX_FORMAT && (reg.value & IDX0_EXPORT_FORMAT_MASK) == SPI_SHADER_2COMP) { + bytes = TwoComponentPayloadBytes; + break; + } + } + *payload = bytes; + return 0; +} + +} diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 0b85ef7fff..dad824f489 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -15,21 +15,6 @@ int APS5_VABI sceAgcAcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcAcbSetWorkloadComplete() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAcbSetWorkloadStreamInactive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAcbSetWorkloadsActive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcAcquireMemSetEngine() { NotImplemented_nid_no_patch(__func__); return 0; @@ -45,26 +30,11 @@ int APS5_VABI sceAgcDcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcDcbSetWorkloadStreamInactive() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcGetDefaultCxStateFlat() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceAgcGetSemaphoreLabel() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcSetAmmSemaphoreMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetSemaphoreMemory() { NotImplemented_nid_no_patch(__func__); return 0; @@ -75,11 +45,6 @@ int APS5_VABI sceAgcCbMemsetExclusive() { return 0; } -int APS5_VABI sceAgcGetGsPrimPayload() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetShaderInstrumentation() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 128553fa40..03aeff9061 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -34,6 +34,10 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* extern "C" int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t stream_id, const void* stream); extern "C" int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t stream_id); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t stream_id, const std::uint32_t* workload_ids, std::uint32_t workload_count); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId); extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); @@ -46,6 +50,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, namespace { +constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); + void check(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); @@ -244,6 +250,59 @@ void testWorkloads() { check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) == nullptr, "workload complete packet after unregistration"); } +void testWorkloadStreams() { + struct Vector { + std::uint32_t streamId; + std::uintptr_t workloadIds; + std::uint32_t workloadCount; + std::uint32_t workloadId; + }; + const std::array vectors{{ + {0x11111111u, 0x22222222u, 0x33333333u, 0x22222222u}, + {1, 2, 3, 2}, + {0x22222222u, static_cast(0x3333333344444444ull), 0, 0x44444444u}}}; + for (const auto& vector : vectors) { + Storage storage; + storage.words.fill(0xdeadbeefu); + const auto before = storage.words; + const auto* ids = reinterpret_cast(vector.workloadIds); + check(sceAgcDcbSetWorkloadsActive(&storage.buffer, vector.streamId, ids, vector.workloadCount) == nullptr, "DCB workloads active returned a packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadComplete(&storage.buffer, vector.streamId, vector.workloadId) == nullptr, "DCB workload complete returned a packet for an unregistered stream"); + check(sceAgcDcbSetWorkloadStreamInactive(&storage.buffer, vector.streamId) == nullptr, "DCB workload stream inactive returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadsActive(&storage.buffer, vector.streamId, ids, vector.workloadCount) == nullptr, "ACB workloads active returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadComplete(&storage.buffer, vector.streamId, vector.workloadId) == nullptr, "ACB workload complete returned a packet for an unregistered stream"); + check(sceAgcAcbSetWorkloadStreamInactive(&storage.buffer, vector.streamId) == nullptr, "ACB workload stream inactive returned a packet for an unregistered stream"); + check(storage.words == before && storage.buffer.cursor_up == storage.words.data(), "a workload call for an unregistered stream wrote to the buffer"); + } + + const std::uint32_t ids[2] = {3, 4}; + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 3; + const auto before = exhausted.words; + check(sceAgcDcbSetWorkloadStreamInactive(&exhausted.buffer, 1) == nullptr && sceAgcAcbSetWorkloadsActive(&exhausted.buffer, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(&exhausted.buffer, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(&exhausted.buffer, 1) == nullptr, + "a workload call for an unregistered stream on an exhausted buffer returned a packet"); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), "a workload call for an unregistered stream modified an exhausted buffer"); + check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, + "a workload call with a null command buffer returned a packet"); + + const std::uint32_t record[8] = {}; + check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); + Storage registered; + registered.words.fill(0xdeadbeefu); + const auto untouched = registered.words; + expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); + expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); + expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); + check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && + sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, + "a workload call with a null command buffer for a registered stream returned a packet"); + check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed"); +} + void testIndexBuffer() { Storage storage; alignas(4) std::uint16_t indices[2]{}; @@ -565,8 +624,10 @@ void testDefaults() { expectFailure([] { sceAgcInit_0090(nullptr, 8); }); expectFailure([&] { sceAgcInit_0090(&state, 14); }); check(sceAgcInit(8) == 0, "AGC version initialization failed"); - expectFailure([] { sceAgcInit(14); }); - expectFailure([] { sceAgcInit(0xffffffffu); }); + check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); + check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument"); + check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument"); + check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); @@ -596,6 +657,7 @@ int main(int argc, char** argv) { testIndexedIndirectDraws(); testMarkers(); testWorkloads(); + testWorkloadStreams(); testIndexBuffer(); testIndirectBase(); testContextState(); diff --git a/core/libs/prx/libSceAgc/tests/GsPrimPayload.cpp b/core/libs/prx/libSceAgc/tests/GsPrimPayload.cpp new file mode 100644 index 0000000000..60f3d29b77 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/GsPrimPayload.cpp @@ -0,0 +1,119 @@ +#include "SceShaders.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" + +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcGetGsPrimPayload(std::uint32_t* payload, const Shader* gs); + +namespace { + +constexpr std::uint32_t Sentinel = 0xccccccccu; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action, const char* message) { + try { + action(); + } catch (const std::exception& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error(message); +} + +Shader makeGs(std::vector& cx) { + Shader gs{}; + gs.cx_registers = cx.empty() ? nullptr : cx.data(); + gs.num_cx_registers = static_cast(cx.size()); + return gs; +} + +void expectPayload(const Shader& gs, std::uint32_t expected, const char* message) { + std::array out{Sentinel, Sentinel}; + check(sceAgcGetGsPrimPayload(out.data(), &gs) == 0, message); + check(out[0] == expected, message); + check(out[1] == Sentinel, "the payload write went past four bytes"); +} + +void testNoRegisters() { + std::vector cx; + expectPayload(makeGs(cx), 0, "a shader without context registers reported a payload"); +} + +void testTwoComponentIndexFormat() { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + expectPayload(makeGs(cx), 8, "a two-component index export did not report 8 bytes"); +} + +void testOnlyTheLowNibbleCounts() { + std::vector high{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 0xfffffff2u}}; + expectPayload(makeGs(high), 8, "bits above IDX0_EXPORT_FORMAT changed the payload"); + for (const std::uint32_t format : {0u, 1u, 3u, 4u, 0x20u}) { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, format}}; + expectPayload(makeGs(cx), 0, "an index export format other than two components reported a payload"); + } +} + +void testOtherRegistersAreIgnored() { + std::vector cx{{ShaderRegs::SPI_VS_OUT_CONFIG, 2u}, {ShaderRegs::GE_MAX_OUTPUT_PER_SUBGROUP, 2u}}; + expectPayload(makeGs(cx), 0, "a register other than SPI_SHADER_IDX_FORMAT reported a payload"); +} + +void testSearchContinuesPastAMismatch() { + std::vector cx{ + {ShaderRegs::SPI_VS_OUT_CONFIG, 0u}, + {ShaderRegs::SPI_SHADER_IDX_FORMAT, 4u}, + {ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}, + }; + expectPayload(makeGs(cx), 8, "a non-matching SPI_SHADER_IDX_FORMAT entry stopped the search"); +} + +void testCountBoundsTheSearch() { + std::vector cx{{ShaderRegs::SPI_VS_OUT_CONFIG, 0u}, {ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + Shader gs = makeGs(cx); + gs.num_cx_registers = 1; + expectPayload(gs, 0, "an entry past num_cx_registers was read"); +} + +void testRejections() { + std::vector cx{{ShaderRegs::SPI_SHADER_IDX_FORMAT, 2u}}; + const Shader gs = makeGs(cx); + std::uint32_t out = Sentinel; + expectFailure([&] { sceAgcGetGsPrimPayload(nullptr, &gs); }, "a null payload pointer was accepted"); + expectFailure([&] { sceAgcGetGsPrimPayload(&out, nullptr); }, "a null shader was accepted"); + Shader missing{}; + missing.num_cx_registers = 1; + expectFailure([&] { sceAgcGetGsPrimPayload(&out, &missing); }, "a register count without a register array was accepted"); + check(out == Sentinel, "a rejected call wrote the payload"); +} + +} + +int main() { + try { + testNoRegisters(); + testTwoComponentIndexFormat(); + testOnlyTheLowNibbleCounts(); + testOtherRegistersAreIgnored(); + testSearchContinuesPastAMismatch(); + testCountBoundsTheSearch(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC GS primitive payload tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/core/libs/prx/libSceAgc/tests/Semaphore.cpp b/core/libs/prx/libSceAgc/tests/Semaphore.cpp new file mode 100644 index 0000000000..2e9e5be0e7 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -0,0 +1,164 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes); +extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label); + +namespace { + +constexpr std::uint64_t blockBytes = 0x4000u; +constexpr std::uint64_t labelBytes = 32u; +constexpr std::uint64_t counterBytes = 8u; +constexpr std::uint64_t memoryBytes = 2u * blockBytes; +constexpr std::uint64_t storageBytes = 4u * blockBytes; +constexpr std::uint32_t labelCount = static_cast(memoryBytes / labelBytes); +constexpr std::uint8_t guardByte = 0xa5u; +constexpr std::uint8_t counterByte = 0x5au; + +struct AlignedRelease { + void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); } +}; + +std::unique_ptr storage; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void expectFailure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +std::uint8_t* memory() { + return storage.get() + blockBytes; +} + +std::vector snapshot() { + return {storage.get(), storage.get() + storageBytes}; +} + +void checkUnchanged(const std::vector& before, const char* message) { + check(std::equal(before.begin(), before.end(), storage.get()), message); +} + +void fillStorage() { + std::fill(storage.get(), storage.get() + storageBytes, guardByte); + for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) { + std::fill(memory() + label, memory() + label + counterBytes, counterByte); + std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0}); + } +} + +void testUnregistered() { + std::array labels{storage.get(), storage.get(), storage.get()}; + expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); }); + expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); + check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output"); +} + +void testRegistrationRejections() { + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); }); + expectFailure([] { + const auto top = std::numeric_limits::max() & ~static_cast(blockBytes - 1u); + sceAgcSetAmmSemaphoreMemory(reinterpret_cast(top), memoryBytes); + }); + checkUnchanged(before, "a rejected registration changed memory"); + testUnregistered(); +} + +void testUnsettledClearing() { + for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) { + memory()[offset] = 0x77u; + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); + checkUnchanged(before, "a registration with unsettled clearing changed memory"); + testUnregistered(); + memory()[offset] = 0; + } +} + +void testRegistration() { + const auto before = snapshot(); + check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed"); + check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters"); + check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory"); + check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory"); +} + +void testLabels() { + for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) { + std::array labels{storage.get(), nullptr, storage.get()}; + check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed"); + check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address"); + check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output"); + } +} + +void testLabelRejections() { + for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits::max()}) { + void* label = storage.get(); + expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); }); + check(label == storage.get(), "a rejected semaphore label changed the output"); + } + expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); }); +} + +void testRepeatedRegistration() { + std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c}); + const auto before = snapshot(); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); }); + expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); }); + checkUnchanged(before, "a repeated registration changed memory"); + void* label = nullptr; + check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory"); +} + +} + +int main() { + try { + storage.reset(static_cast(::operator new(storageBytes, std::align_val_t{blockBytes}))); + fillStorage(); + testUnregistered(); + testRegistrationRejections(); + testUnsettledClearing(); + testRegistration(); + testLabels(); + testLabelRejections(); + testRepeatedRegistration(); + storage.reset(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC semaphore tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + try { LibcRunShutdown_nid_postfix(); } + catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 24a4686034..8c49c9589c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -162,6 +162,8 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderMemory.cpp @@ -219,9 +221,22 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Submit/src/Dcb.cpp ) +find_package(Git QUIET) +set(agcTraceRevisionHeader ${agcTextureDetileGeneratedRoot}/prx/libSceAgcDriver/TraceRevision.hpp) +add_custom_target(agc_trace_revision + COMMAND ${CMAKE_COMMAND} -DGIT=${GIT_EXECUTABLE} -DSOURCE=${CMAKE_SOURCE_DIR} -DOUTPUT=${agcTraceRevisionHeader} -P ${CMAKE_CURRENT_SOURCE_DIR}/trace_revision.cmake + BYPRODUCTS ${agcTraceRevisionHeader} + VERBATIM +) +set(agcZlibIncludeDir ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) +set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp PROPERTIES + INCLUDE_DIRECTORIES ${agcZlibIncludeDir} +) + add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${agcDriverSources} ) +add_dependencies(libSceAgcDriver agc_trace_revision) target_include_directories(libSceAgcDriver PRIVATE ${LIBS_INCLUDE_DIR} @@ -247,8 +262,12 @@ target_link_libraries(libSceAgcDriver PRIVATE SDL2-static libc libkernel + jpeg_turbo ${CMAKE_DL_LIBS} ) +if(APS5_ENABLE_TRACY) + target_link_libraries(libSceAgcDriver PUBLIC TracyClient) +endif() set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceAgcDriver) @@ -274,16 +293,61 @@ if(BUILD_TESTING) set_tests_properties(agc_profile_output PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp) + target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_stack_walk_tests PRIVATE libc) + configure_windows_unwind(agc_stack_walk_tests) + add_test(NAME agc_stack_walk COMMAND agc_stack_walk_tests) + set_tests_properties(agc_stack_walk PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_stack_walk PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(agc_performance_trace_tests tests/PerformanceTrace.cpp Execution/src/TraceFile.cpp) + target_include_directories(agc_performance_trace_tests PRIVATE ${LIBS_INCLUDE_DIR} ${agcZlibIncludeDir}) + target_link_libraries(agc_performance_trace_tests PRIVATE libc jpeg_turbo) + configure_windows_unwind(agc_performance_trace_tests) + add_test(NAME agc_performance_trace COMMAND agc_performance_trace_tests) + set_tests_properties(agc_performance_trace PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) - set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Lifecycle.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) endif() foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(agc_driver_register_key_tests EXCLUDE_FROM_ALL tests/RegisterKey.cpp) +target_include_directories(agc_driver_register_key_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_driver_register_key_tests PRIVATE + libSceAgcDriver + libc + libkernel +) +set_target_properties(agc_driver_register_key_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_driver_register_key_tests) +if(BUILD_TESTING) + set_target_properties(agc_driver_register_key_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_driver_register_key COMMAND agc_driver_register_key_tests) + set_tests_properties(agc_driver_register_key PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_VERIFY_REGISTER_KEYS=1") + if(WIN32) + set_tests_properties(agc_driver_register_key PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL tests/Submit.cpp ) @@ -466,6 +530,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) +target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_spirv_branch_targets_tests) +if(BUILD_TESTING) + set_target_properties(agc_spirv_branch_targets_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_spirv_branch_targets COMMAND agc_spirv_branch_targets_tests) + set_tests_properties(agc_spirv_branch_targets PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_spirv_branch_targets PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp) target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -489,6 +566,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_masked_select_tests) +add_test_executable(agc_ir_reclaim_tests EXCLUDE_FROM_ALL tests/IrReclaim.cpp) +target_link_libraries(agc_ir_reclaim_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_ir_reclaim_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_ir_reclaim_tests) + add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp) target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -499,6 +581,11 @@ target_link_libraries(agc_constant_bit_test_tests PRIVATE shader_recompiler_agc_ set_target_properties(agc_constant_bit_test_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_constant_bit_test_tests) +add_test_executable(agc_integer_narrowing_tests EXCLUDE_FROM_ALL tests/IntegerNarrowing.cpp) +target_link_libraries(agc_integer_narrowing_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_integer_narrowing_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_integer_narrowing_tests) + add_test_executable(agc_scalar_select_mask_tests EXCLUDE_FROM_ALL tests/ScalarSelectMask.cpp) target_link_libraries(agc_scalar_select_mask_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_scalar_select_mask_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -646,6 +733,11 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_formatted_buffer_byte_stores_tests EXCLUDE_FROM_ALL tests/FormattedBufferByteStores.cpp) +target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_formatted_buffer_byte_stores_tests) + add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp) target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc) @@ -775,6 +867,19 @@ target_link_libraries(agc_driver_cmask_fast_clear_tests PRIVATE set_target_properties(agc_driver_cmask_fast_clear_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_cmask_fast_clear_tests) +set(agcBufferUpdateSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/BufferUpdate.comp) +set(agcBufferUpdateSpv ${agcTestShaderGeneratedRoot}/BufferUpdate.spv) +set(agcBufferUpdateHeader ${agcTestShaderGeneratedRoot}/BufferUpdate_spv.h) +add_custom_command( + OUTPUT "${agcBufferUpdateHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcBufferUpdateSpv}" "${agcBufferUpdateSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcBufferUpdateSpv} -DOUTPUT=${agcBufferUpdateHeader} + -DSYMBOL=BUFFER_UPDATE_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcBufferUpdateSource}" glslang-standalone + VERBATIM +) + add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp tests/ResidentPresent.cpp @@ -782,6 +887,7 @@ add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL ${agcSampleLodShaderHeader} ${agcSampleArrayShaderHeader} ${agcCoverShaderHeaders} + ${agcBufferUpdateHeader} ) target_include_directories(agc_driver_recorder_tests PRIVATE @@ -825,6 +931,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) + set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_integer_clear PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only) add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only) set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) @@ -837,12 +949,31 @@ if(BUILD_TESTING) set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_written_buffer_order COMMAND agc_driver_recorder_tests --written-buffer-order-only) + set_tests_properties(agc_storage_written_buffer_order PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_written_buffer_order PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_copied_direct_ordering COMMAND agc_driver_recorder_tests --copied-direct-only) + set_tests_properties(agc_copied_direct_ordering PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_host_import_unmap COMMAND agc_driver_recorder_tests --host-import-unmap-only) set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only) set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + add_test(NAME agc_host_import_retirement COMMAND agc_driver_recorder_tests --host-import-retirement-only) + 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) @@ -852,7 +983,7 @@ if(BUILD_TESTING) 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) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap 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_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 "PATH=path_list_prepend:$") endif() endif() @@ -914,47 +1045,11 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp tests/DepthSurfaceReuse.cpp - Graphics/src/Draw.cpp - Graphics/src/DepthSurface.cpp - Graphics/src/MultisampleTarget.cpp - Graphics/src/Pipeline.cpp - Graphics/src/PipelineLibrary.cpp - Graphics/src/State.cpp - Graphics/src/ColorTargetLayout.cpp - Graphics/src/ColorTargetTransfer.cpp - Graphics/src/Resources.cpp - Graphics/src/PlaceholderImages.cpp - Graphics/src/ShaderResources.cpp - Graphics/src/ShaderAddressResources.cpp - Graphics/src/GuestBufferMemory.cpp - Graphics/src/BdaResources.cpp - Graphics/src/BufferAddress.cpp - Graphics/src/ShaderValidation.cpp - Graphics/src/GuestTextureResource.cpp - Graphics/src/GuestSamplerResource.cpp - Graphics/src/TextureFormat.cpp - Graphics/src/DccMetadata.cpp - Graphics/src/TextureTiling.cpp - Graphics/src/TextureDetiler.cpp - Graphics/src/Texture.cpp - Graphics/src/PassHazards.cpp - Graphics/src/UnitShadow.cpp - Graphics/src/Sampler.cpp - Graphics/src/Recorder.cpp - Graphics/src/ParallelCompare.cpp - Graphics/src/ShaderInputState.cpp - ${agcTextureDetileShaderHeader} - ${agcCmaskClearShaderHeader} - ${agcMeshArgumentsShaderHeader} - ${agcTextureDetileImageShaderHeader} - ${agcSampleCounterShaderHeader} - ${agcSampleDumpsShaderHeader} - Execution/src/GuestMemory.cpp - Execution/src/Pm4.cpp ) target_include_directories(agc_driver_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR} + $ ${agcTextureDetileGeneratedRoot} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/core/shader/recompiler/IntermediateRepresentation/include @@ -965,7 +1060,7 @@ target_include_directories(agc_driver_graphics_tests PRIVATE ) target_link_libraries(agc_driver_graphics_tests PRIVATE - shader_recompiler_agc_driver + libSceAgcDriver libc libkernel ) @@ -973,44 +1068,71 @@ target_link_libraries(agc_driver_graphics_tests PRIVATE set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_graphics_tests) +foreach(agcVertexRangeShader IN ITEMS VertexRange.vert VertexRangeProducer.frag) + string(REPLACE "." "_" agcVertexRangeName "${agcVertexRangeShader}") + string(TOUPPER "${agcVertexRangeName}_SPV" agcVertexRangeSymbol) + set(agcVertexRangeSpv ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}.spv) + set(agcVertexRangeHeader ${agcTestShaderGeneratedRoot}/${agcVertexRangeName}_spv.h) + add_custom_command( + OUTPUT "${agcVertexRangeHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcVertexRangeSpv}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcVertexRangeSpv} -DOUTPUT=${agcVertexRangeHeader} + -DSYMBOL=${agcVertexRangeSymbol} -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcVertexRangeShader}" glslang-standalone + VERBATIM + ) + list(APPEND agcVertexRangeHeaders ${agcVertexRangeHeader}) +endforeach() + +target_sources(agc_driver_recorder_tests PRIVATE ${agcVertexRangeHeaders}) + +if(BUILD_TESTING) + add_test(NAME agc_driver_vertex_target_range_copied COMMAND agc_driver_recorder_tests --vertex-target-range-copied) + add_test(NAME agc_driver_vertex_target_range_imported COMMAND agc_driver_recorder_tests --vertex-target-range-imported) + set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_vertex_target_range_copied agc_driver_vertex_target_range_imported PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaDevice.cpp tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp - Execution/src/BdaFeatures.cpp - Graphics/src/Resources.cpp - Graphics/src/PlaceholderImages.cpp - Graphics/src/BufferAddress.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE ${LIBS_INCLUDE_DIR} + $ ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ) -target_link_libraries(agc_driver_bda_device_tests PRIVATE SDL2-static libc shader_recompiler_agc_driver) +target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) -add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL - tests/Mesh.cpp - ${agcDriverSources} -) +add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE ${LIBS_INCLUDE_DIR} - ${agcTextureDetileGeneratedRoot} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} ) -target_link_libraries(agc_driver_mesh_tests PRIVATE shader_recompiler_agc_driver SDL2-static libc libkernel ${CMAKE_DL_LIBS}) +target_link_libraries(agc_driver_mesh_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_mesh_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_mesh_tests) +set(ANYPS5_AGC_EXECUTION_TESTS "" CACHE STRING "tests/execution sources to build, by file name without extension; the others build a stub that reports a skip. Empty builds all") file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/*.cpp) set(agcExecutionTests) foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE) + if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS) + set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp) + endif() string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" agcExecutionTestName "${agcExecutionTestName}") string(TOLOWER "agc_driver_${agcExecutionTestName}_tests" agcExecutionTest) add_test_executable(${agcExecutionTest} EXCLUDE_FROM_ALL ${agcExecutionTestSource}) @@ -1025,6 +1147,13 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) configure_windows_unwind(${agcExecutionTest}) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) + if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) + message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") + endif() +endforeach() + +target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) @@ -1093,7 +1222,7 @@ if(BUILD_TESTING) 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:$") 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_vector_window_tests agc_constant_bit_test_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 ${agcExecutionTests}) + 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") continue() endif() @@ -1106,6 +1235,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + 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) + if(WIN32) + set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" @@ -1145,11 +1280,25 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_vector_window_stage_inputs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_layered_volume_vertex COMMAND agc_driver_layered_volume_draw_tests --vertex-only) + add_test(NAME agc_driver_layered_volume_vertex_no_gpl COMMAND agc_driver_layered_volume_draw_tests --vertex-only) + set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_property(TEST agc_driver_layered_volume_vertex_no_gpl APPEND PROPERTY ENVIRONMENT "APS5_NO_GPL=1") + if(WIN32) + set_tests_properties(agc_driver_layered_volume_vertex agc_driver_layered_volume_vertex_no_gpl PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv) + set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests) set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests) @@ -1168,6 +1317,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_uint8x4_color_target_no_resident COMMAND agc_driver_uint8x4_color_target_tests) + set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_RESIDENT_TARGETS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flatAperturesPart RANGE 13) @@ -1189,6 +1344,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex + # stage, which must include PA_CL_VS_OUT_CNTL. + add_test(NAME agc_driver_vertex_clip_cull_draw_no_key COMMAND agc_driver_vertex_clip_cull_draw_tests no-key) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_DRAW_KEY=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() 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) @@ -1334,7 +1497,9 @@ if(AGC_BUILD_VISUAL_TEST) SDL2-static libc libkernel + jpeg_turbo ) + add_dependencies(agc_driver_visual_test agc_trace_revision) set_target_properties(agc_driver_visual_test PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_visual_test) @@ -1405,8 +1570,11 @@ if(BUILD_TESTING) configure_windows_unwind(agc_compute_pipeline_capture_tests) add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state) + set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) @@ -1468,6 +1636,15 @@ if(BUILD_TESTING) set_tests_properties(guest_agc_driver_resources PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_agc_driver_debug_unavailable_tests ${LIBS_INCLUDE_DIR}/tests/GuestAgcDriverDebugUnavailable.cpp) + target_include_directories(guest_agc_driver_debug_unavailable_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_agc_driver_debug_unavailable_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(guest_agc_driver_debug_unavailable_tests) + add_test(NAME guest_agc_driver_debug_unavailable COMMAND guest_agc_driver_debug_unavailable_tests) + if(WIN32) + set_tests_properties(guest_agc_driver_debug_unavailable PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_rendering_wait_tests ${LIBS_INCLUDE_DIR}/tests/GuestRenderingWait.cpp) target_include_directories(guest_rendering_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_rendering_wait_tests PRIVATE libSceAgcDriver libc) @@ -1702,6 +1879,21 @@ add_test_executable(agc_position_invariance_tests EXCLUDE_FROM_ALL tests/Positio target_link_libraries(agc_position_invariance_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_position_invariance_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_position_invariance_tests) + +add_test_executable(agc_vertex_layer_export_tests EXCLUDE_FROM_ALL tests/VertexLayerExport.cpp) +target_link_libraries(agc_vertex_layer_export_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_layer_export_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_layer_export_tests) +if(BUILD_TESTING) + set_target_properties(agc_vertex_layer_export_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vertex_layer_export COMMAND agc_vertex_layer_export_tests) + set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_vertex_layer_export PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) set_target_properties(agc_position_invariance_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_position_invariance COMMAND agc_position_invariance_tests) 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..859344a126 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -75,6 +75,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/Draw/DrawRegisterKey.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp new file mode 100644 index 0000000000..74a562c8b5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP + +#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" + +namespace AgcDriver::DriverDetail { + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index b5659313bf..c305a1c933 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -44,6 +44,7 @@ namespace AgcDriver::DriverDetail { class Driver { public: static Driver& Get(); + static std::uint64_t GpuMapImportThreads(); ~Driver(); void Shutdown(); void WaitIdle(); @@ -114,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); @@ -309,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 79edd8b461..025aa35c42 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp @@ -18,6 +18,7 @@ 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); } 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 ad90c2db71..51334acc72 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include +#include #include #include #include @@ -66,6 +67,7 @@ struct PreparedShaders : PreparedShaderState { std::mutex mutex; std::condition_variable settled; std::exception_ptr failure; + std::atomic demanded{false}; bool pending = false; }; @@ -84,6 +86,7 @@ struct ShaderSnapshot { std::shared_ptr prepared = std::make_shared(); std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); + std::shared_ptr> positionVectorUse = std::make_shared>(0u); }; @@ -92,6 +95,9 @@ std::shared_ptr ProgramSnapshot(const ShaderRegistry& shad 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); bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); @@ -102,7 +108,8 @@ std::optional RegisteredFloatMode(const Shade std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader); std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); -void SubmitRegistrationPreparation(std::function task); +void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared = nullptr); +void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 740f1f45e8..3932b5dc58 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -30,6 +30,7 @@ void Read(std::uint64_t address, std::span destination, std::size_t a // Variants for resources in heaps the guest commits on demand (partially resident textures, GPU // scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored. void ReadCommitted(std::uint64_t address, std::span destination); +void ReadCommittedUnsynced(std::uint64_t address, std::span destination); bool EqualsCommitted(std::uint64_t address, std::span bytes); // EqualsCommitted without the flush hook: the caller has decided that the recorded GPU work // writing the range need not be waited for (the dispatch-cache validation in Driver.cpp). @@ -59,7 +60,7 @@ void WriteChanged(std::uint64_t address, std::span current, std // UnchangedSince is always false, so callers fall back to comparing bytes. 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 ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); 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); @@ -115,7 +116,6 @@ bool UnchangedSinceCollected(std::uint64_t address, std::size_t bytes, std::uint // watched arena, or for a generation of 0, a block reads as changed. constexpr std::uint8_t BlockUnchanged = 0; constexpr std::uint8_t BlockWritten = 1; -constexpr std::uint8_t BlockMaybeWritten = 2; bool ChangedBlocks(std::uint64_t address, std::size_t bytes, std::span generations, std::span changed, std::span cpu = {}); // Serializes device work: draws, dispatches, presentation and the deferred write-backs below. The diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp index 1f6bb75a0b..28ccc5c2df 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETIMER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETIMER_HPP +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include #include #include @@ -258,11 +260,11 @@ private: class PerformanceTimer { public: - explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), scope(scope) { + explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), trace(PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr), scope(scope) { if (frame != nullptr) { total = frame->Get(scope, "total"); } - if (frame != nullptr) start = Clock::now(); + if (frame != nullptr || trace != nullptr) start = Clock::now(); previous = start; } @@ -274,8 +276,12 @@ public: } void Finish() { - if (frame == nullptr) return; + zone.End(); + if (frame == nullptr && trace == nullptr) return; const auto end = Clock::now(); + if (trace != nullptr) trace->Add(scope, nullptr, start, end); + trace = nullptr; + if (frame == nullptr) return; if (tail != nullptr) frame->Add(tail, end - previous); frame->Add(total, end - start); frame = nullptr; @@ -287,10 +293,13 @@ public: } void Mark(const char* stage, std::uint64_t bytes = 0) { - if (frame == nullptr) return; + if (frame == nullptr && trace == nullptr) return; const auto now = Clock::now(); - if (tail == nullptr) tail = frame->Get(scope, "tail"); - frame->Add(frame->Get(scope, stage), now - previous, bytes); + if (trace != nullptr) trace->Add(scope, stage, previous, now); + if (frame != nullptr) { + if (tail == nullptr) tail = frame->Get(scope, "tail"); + frame->Add(frame->Get(scope, stage), now - previous, bytes); + } previous = now; } @@ -298,7 +307,9 @@ private: using Clock = FrameTiming::Clock; FrameTiming* frame; + TraceThread* trace; const char* scope; + [[no_unique_address]] TracyZone zone{scope}; FrameTiming::Metric* total = nullptr; FrameTiming::Metric* tail = nullptr; Clock::time_point start{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp new file mode 100644 index 0000000000..b859482795 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -0,0 +1,437 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +class VulkanDevice; + +class TraceWindow { +public: + using Clock = std::chrono::steady_clock; + enum class Edge { None, Open, Close }; + + static std::optional Parse(const char* frames, const char* seconds) { + if (frames == nullptr && seconds == nullptr) return std::nullopt; + if (frames != nullptr && seconds != nullptr) throw std::invalid_argument("APS5_TRACE_FRAMES and APS5_TRACE_SECONDS cannot be set together"); + const std::string_view name = frames != nullptr ? "APS5_TRACE_FRAMES" : "APS5_TRACE_SECONDS"; + const std::string_view text = frames != nullptr ? frames : seconds; + const auto colon = text.find(':'); + std::uint64_t start = 0; + std::uint64_t length = 0; + if (colon == std::string_view::npos || !number(text.substr(0, colon), start) || !number(text.substr(colon + 1), length) || length == 0) + throw std::invalid_argument(std::string(name) + "=" + std::string(text) + " is not : with a length above 0"); + return TraceWindow(frames != nullptr, start, length); + } + + Edge Present(std::uint64_t present, Clock::duration elapsed) { + if (state == State::Closed) return Edge::None; + if (state == State::Waiting) { + if (frames ? present < start : wholeSeconds(elapsed) < start) return Edge::None; + state = State::Open; + openedPresent = present; + openedAt = elapsed; + return Edge::Open; + } + if (frames ? present - openedPresent < length : wholeSeconds(elapsed - openedAt) < length) return Edge::None; + state = State::Closed; + return Edge::Close; + } + + std::string Describe() const { + return std::string(frames ? "frames " : "seconds ") + std::to_string(start) + ":" + std::to_string(length); + } + +private: + enum class State { Waiting, Open, Closed }; + + TraceWindow(bool frames, std::uint64_t start, std::uint64_t length) : frames(frames), start(start), length(length) {} + + static bool number(std::string_view text, std::uint64_t& value) { + const auto end = text.data() + text.size(); + const auto [last, error] = std::from_chars(text.data(), end, value); + return !text.empty() && error == std::errc{} && last == end; + } + + static std::uint64_t wholeSeconds(Clock::duration elapsed) { + return static_cast(std::chrono::duration_cast(elapsed).count()); + } + + bool frames; + std::uint64_t start; + std::uint64_t length; + State state = State::Waiting; + std::uint64_t openedPresent = 0; + Clock::duration openedAt{}; +}; + +class TraceThread { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::size_t DefaultCapacity = 262144; + static constexpr Clock::duration MinimumSlice = std::chrono::microseconds(20); + + struct Slice { + const char* scope; + const char* stage; + Clock::time_point begin; + Clock::duration length; + }; + + struct Total { + const char* scope; + const char* stage; + Clock::duration length; + std::uint64_t count; + }; + + struct Snapshot { + std::string name; + std::span slices; + std::vector totals; + 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) {} + + TraceThread(const TraceThread&) = delete; + TraceThread& operator=(const TraceThread&) = delete; + + 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); + const auto present = presents.load(std::memory_order_relaxed); + if (!sampled || present != sampledPresent) { + if (sampled) append({nullptr, nullptr, sampleBegin, folded}); + sampled = true; + sampledPresent = present; + sampleBegin = begin; + folded = {}; + } + if (length < MinimumSlice) folded += length; + else append({scope, stage, begin, length}); + } + + Snapshot Collect() const { + Snapshot snapshot{name, {slices.get(), used.load(std::memory_order_acquire)}, {}, false}; + for (const auto& entry : entries) { + const auto* scope = entry.scope.load(std::memory_order_acquire); + if (scope == nullptr) continue; + snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); + } + snapshot.dropped = dropped.load(std::memory_order_relaxed); + return snapshot; + } + +private: + struct Entry { + std::atomic scope{nullptr}; + std::atomic stage{nullptr}; + std::atomic length{0}; + std::atomic count{0}; + }; + + void append(const Slice& slice) { + const auto index = used.load(std::memory_order_relaxed); + if (index == capacity) { + dropped.store(true, std::memory_order_relaxed); + return; + } + slices[index] = slice; + used.store(index + 1, std::memory_order_release); + } + + void account(const char* scope, const char* stage, Clock::duration length) { + const auto key = reinterpret_cast(scope) ^ (reinterpret_cast(stage) >> 3); + const auto hash = static_cast((static_cast(key) * 0x9e3779b97f4a7c15ull) >> 55); + for (std::size_t probe = 0; probe < entries.size(); ++probe) { + auto& entry = entries[(hash + probe) % entries.size()]; + const auto* current = entry.scope.load(std::memory_order_relaxed); + if (current == nullptr) { + entry.stage.store(stage, std::memory_order_relaxed); + entry.scope.store(scope, std::memory_order_release); + } else if (current != scope || entry.stage.load(std::memory_order_relaxed) != stage) { + continue; + } + entry.length.store(entry.length.load(std::memory_order_relaxed) + length.count(), std::memory_order_relaxed); + entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); + return; + } + dropped.store(true, std::memory_order_relaxed); + } + + const std::string name; + const std::atomic& presents; + const std::unique_ptr slices; + const std::size_t capacity; + std::atomic used{0}; + std::atomic dropped{false}; + std::array entries; + bool sampled = false; + std::uint64_t sampledPresent = 0; + Clock::time_point sampleBegin{}; + Clock::duration folded{}; +}; + +struct TraceSummary { + std::string commit; + std::optional dirty; + std::vector> environment; + std::string gpu; + std::string driver; + std::string os; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::string window; + std::uint64_t firstPresent = 0; + double gpuBusyMs = 0; + std::string trace; +}; + +struct TraceGpuRange { + const char* name; + double beginNs; + double endNs; +}; + +struct TraceClockSample { + double gpuNs = 0; + std::chrono::steady_clock::time_point cpu{}; +}; + +class TraceFormat { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::array WorkerBusyScopes{"Driver.Packet", "Driver.Suspend", "Driver.EndSubmission", "Driver.Rewind", "Driver.Completion"}; + static constexpr std::string_view WorkerWaitScope = "Driver.WorkerWait"; + static constexpr std::string_view WorkerPrefix = "queue worker "; + static constexpr std::size_t TopPhases = 8; + + static void AppendString(std::string& output, std::string_view text) { + output += '"'; + for (const char character : text) { + const auto byte = static_cast(character); + if (character == '"' || character == '\\') { + output += '\\'; + output += character; + } else if (byte < 0x20 || byte >= 0x7f) { + char escaped[8]; + std::snprintf(escaped, sizeof escaped, "\\u%04x", static_cast(byte)); + output += escaped; + } else { + output += character; + } + } + output += '"'; + } + + static std::string Name(const char* scope, const char* stage) { + return stage == nullptr ? std::string(scope) : std::string(scope) + "." + stage; + } + + static std::string Events(std::uint64_t process, Clock::time_point origin, std::span threads, std::size_t presenter, std::span presents, std::uint64_t firstPresent) { + std::string output = "{\"traceEvents\":[\n"; + bool first = true; + const auto event = [&](std::string_view name, const char* phase, Clock::time_point at, std::size_t tid) { + if (!first) output += ",\n"; + first = false; + output += "{\"name\":"; + AppendString(output, name); + char text[96]; + std::snprintf(text, sizeof text, ",\"ph\":\"%s\",\"ts\":%.3f,\"pid\":%llu,\"tid\":%llu", phase, microseconds(at - origin), static_cast(process), static_cast(tid)); + output += text; + }; + event("process_name", "M", origin, 0); + output += ",\"args\":{\"name\":\"AnyPS5\"}}"; + for (std::size_t index = 0; index < threads.size(); ++index) { + const auto& thread = threads[index]; + const auto tid = index + 1; + event("thread_name", "M", origin, tid); + output += ",\"args\":{\"name\":"; + AppendString(output, thread.name); + output += "}}"; + std::vector slices(thread.slices.begin(), thread.slices.end()); + std::stable_sort(slices.begin(), slices.end(), [](const auto& a, const auto& b) { return a.begin != b.begin ? a.begin < b.begin : a.length > b.length; }); + const auto folded = thread.name + " folded"; + for (const auto& slice : slices) { + char text[64]; + if (slice.scope == nullptr) { + event(folded, "C", slice.begin, tid); + std::snprintf(text, sizeof text, ",\"args\":{\"ms\":%.3f}}", milliseconds(slice.length)); + } else { + event(Name(slice.scope, slice.stage), "X", slice.begin, tid); + std::snprintf(text, sizeof text, ",\"dur\":%.3f}", microseconds(slice.length)); + } + output += text; + } + } + for (std::size_t index = 0; index + 1 < presents.size(); ++index) { + event("present", "i", presents[index], presenter + 1); + output += ",\"s\":\"g\",\"args\":{\"present\":" + std::to_string(firstPresent + index) + "}}"; + } + output += "\n]}\n"; + return output; + } + + static std::vector GpuSlices(std::span ranges, const TraceClockSample& first, const TraceClockSample& last, Clock::time_point origin) { + const double gpuSpan = last.gpuNs - first.gpuNs; + const double scale = gpuSpan > 0 ? std::chrono::duration(last.cpu - first.cpu).count() / gpuSpan : 1.0; + const auto map = [&](double gpuNs) { return first.cpu + std::chrono::duration_cast(std::chrono::duration((gpuNs - first.gpuNs) * scale)); }; + std::vector slices; + for (const auto& range : ranges) { + const auto begin = map(range.beginNs); + const auto end = map(range.endNs); + if (end <= begin || begin < origin) continue; + slices.push_back({range.name, nullptr, begin, end - begin}); + } + return slices; + } + + 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(); + std::vector frames; + for (std::size_t index = 1; index < presents.size(); ++index) frames.push_back(milliseconds(presents[index] - presents[index - 1])); + std::sort(frames.begin(), frames.end()); + const auto percentile = [&](double fraction) { return frames.empty() ? 0.0 : frames[std::min(frames.size() - 1, static_cast(fraction * static_cast(frames.size())))]; }; + const double perPresent = count != 0 ? 1.0 / static_cast(count) : 0.0; + + std::string output = "{\"commit\":"; + AppendString(output, summary.commit); + output += ",\"dirty\":"; + output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null"; + output += ",\"env\":{"; + for (std::size_t index = 0; index < summary.environment.size(); ++index) { + if (index != 0) output += ','; + AppendString(output, summary.environment[index].first); + output += ':'; + AppendString(output, summary.environment[index].second); + } + output += "},\"gpu\":"; + AppendString(output, summary.gpu); + output += ",\"driver\":"; + AppendString(output, summary.driver); + output += ",\"os\":"; + AppendString(output, summary.os); + output += ",\"resolution\":"; + AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height)); + output += ",\"window\":{\"spec\":"; + AppendString(output, summary.window); + output += ",\"first_present\":" + std::to_string(summary.firstPresent); + output += ",\"ms\":" + number(milliseconds(span)) + "}"; + 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); + + output += ",\"worker_busy_pct\":{"; + bool firstWorker = true; + std::map phases; + bool dropped = false; + for (const auto& thread : threads) { + dropped = dropped || thread.dropped; + Clock::duration busy{}; + for (const auto& total : thread.totals) { + const std::string_view scope = total.scope; + if (total.stage == nullptr && std::find(WorkerBusyScopes.begin(), WorkerBusyScopes.end(), scope) != WorkerBusyScopes.end()) busy += total.length; + if (scope != WorkerWaitScope) phases[Name(total.scope, total.stage)] += total.length; + } + if (!thread.name.starts_with(WorkerPrefix)) continue; + if (!firstWorker) output += ','; + firstWorker = false; + AppendString(output, thread.name); + output += ':' + number(span != Clock::duration{} ? 100.0 * static_cast(busy.count()) / static_cast(span.count()) : 0.0); + } + output += "},\"top_phases_ms_per_frame\":{"; + std::vector> top(phases.begin(), phases.end()); + std::stable_sort(top.begin(), top.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); + for (std::size_t index = 0; index < top.size() && index < TopPhases; ++index) { + if (index != 0) output += ','; + AppendString(output, top[index].first); + output += ':' + number(milliseconds(top[index].second) * perPresent); + } + output += "},\"trace_dropped\":"; + output += dropped ? "true" : "false"; + output += ",\"trace\":"; + AppendString(output, summary.trace); + output += "}\n"; + return output; + } + +private: + static double microseconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); + } + + static double milliseconds(Clock::duration elapsed) { + return std::chrono::duration(elapsed).count(); + } + + static std::string number(double value) { + char text[32]; + std::snprintf(text, sizeof text, "%.3f", value); + return text; + } +}; + +class PerformanceTrace { +public: + static bool Configured() { + static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr; + return configured; + } + + static void BindThread(const char* name, std::size_t capacity = TraceThread::DefaultCapacity); + 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; + std::lock_guard lock(gpuMutex); + if (gpuRanges.size() == gpuRanges.capacity()) { + gpuDropped = true; + return; + } + gpuRanges.push_back({name, beginNs, endNs}); + } + + static bool Recording() { + return recording.load(std::memory_order_relaxed); + } + + static TraceThread* Thread() { + return local; + } + + static void StopOutput(); + +private: + inline static std::atomic recording{false}; + inline static std::atomic presentCount{0}; + inline static thread_local TraceThread* local = nullptr; + inline static std::mutex gpuMutex; + inline static std::vector gpuRanges; + inline static bool gpuDropped = false; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 4e97437733..fabe4a59a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,29 @@ public: using mapped_type = std::uint32_t; using value_type = std::pair; + class Mask { + public: + explicit Mask(std::vector bits) : bits(std::move(bits)) {} + bool Contains(std::uint32_t offset) const { return offset / 64 < bits.size() && ((bits[offset / 64] >> (offset % 64)) & 1u) != 0; } + private: + std::vector bits; + }; + + class Reference { + public: + Reference(Registers& owner, std::uint32_t offset) : owner(owner), offset(offset) {} + operator std::uint32_t() const { return owner.values[offset]; } + Reference& operator=(std::uint32_t value) { owner.insert_or_assign(offset, value); return *this; } + Reference& operator=(const Reference& other) { return *this = static_cast(other); } + Reference& operator|=(std::uint32_t value) { return *this = static_cast(*this) | value; } + Reference& operator&=(std::uint32_t value) { return *this = static_cast(*this) & value; } + Reference& operator^=(std::uint32_t value) { return *this = static_cast(*this) ^ value; } + Reference& operator+=(std::uint32_t value) { return *this = static_cast(*this) + value; } + private: + Registers& owner; + std::uint32_t offset; + }; + class const_iterator { public: using iterator_category = std::forward_iterator_tag; @@ -57,6 +81,18 @@ public: using iterator = const_iterator; Registers() = default; + Registers(const Registers&) = default; + Registers& operator=(const Registers&) = default; + Registers(Registers&& other) noexcept : values(std::move(other.values)), present(std::move(other.present)), entries(std::exchange(other.entries, 0)), mask(std::move(other.mask)), fingerprint(std::exchange(other.fingerprint, 0)) {} + Registers& operator=(Registers&& other) noexcept { + if (this == &other) return *this; + values = std::move(other.values); + present = std::move(other.present); + entries = std::exchange(other.entries, 0); + mask = std::move(other.mask); + fingerprint = std::exchange(other.fingerprint, 0); + return *this; + } Registers(std::initializer_list> entries) { for (const auto& [offset, value] : entries) emplace(offset, value); } @@ -83,27 +119,33 @@ public: values.clear(); present.clear(); entries = 0; + fingerprint = 0; } std::pair emplace(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) return {const_iterator{this, offset}, false}; mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } std::pair insert_or_assign(std::uint32_t offset, std::uint32_t value) { if (contains(offset)) { + if (values[offset] == value) return {const_iterator{this, offset}, false}; + changeFingerprint(offset, values[offset]); values[offset] = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, false}; } mark(offset) = value; + changeFingerprint(offset, value); return {const_iterator{this, offset}, true}; } - std::uint32_t& operator[](std::uint32_t offset) { - if (contains(offset)) return values[offset]; - return mark(offset) = 0; + Reference operator[](std::uint32_t offset) { + if (!contains(offset)) emplace(offset, 0); + return {*this, offset}; } - std::uint32_t& at(std::uint32_t offset) { + Reference at(std::uint32_t offset) { if (!contains(offset)) throw std::out_of_range("register is not set"); - return values[offset]; + return {*this, offset}; } const std::uint32_t& at(std::uint32_t offset) const { if (!contains(offset)) throw std::out_of_range("register is not set"); @@ -111,6 +153,7 @@ public: } std::size_t erase(std::uint32_t offset) { if (!contains(offset)) return 0; + changeFingerprint(offset, values[offset]); present[offset / 64] &= ~(std::uint64_t{1} << (offset % 64)); --entries; return 1; @@ -123,8 +166,31 @@ public: } return a == end() && b == other.end(); } + std::uint64_t Fingerprint(const std::shared_ptr& selected) const { + if (mask != selected) { + fingerprint = RecomputeFingerprint(*selected); + mask = selected; + } + return fingerprint; + } + std::uint64_t RecomputeFingerprint(const Mask& selected) const { + std::uint64_t result = 0; + for (const auto [offset, value] : *this) { + if (selected.Contains(offset)) result ^= contribution(offset, value); + } + return result; + } private: + static std::uint64_t contribution(std::uint32_t offset, std::uint32_t value) { + auto hash = ((static_cast(offset) << 32u) | value) + 0x9e3779b97f4a7c15ull; + hash = (hash ^ (hash >> 30u)) * 0xbf58476d1ce4e5b9ull; + hash = (hash ^ (hash >> 27u)) * 0x94d049bb133111ebull; + return hash ^ (hash >> 31u); + } + void changeFingerprint(std::uint32_t offset, std::uint32_t value) { + if (mask != nullptr && mask->Contains(offset)) fingerprint ^= contribution(offset, value); + } static constexpr std::size_t End = ~std::size_t{0}; std::uint32_t& mark(std::uint32_t offset) { if (offset >= values.size()) { @@ -147,6 +213,8 @@ private: std::vector values; std::vector present; std::size_t entries = 0; + mutable std::shared_ptr mask; + mutable std::uint64_t fingerprint = 0; }; inline Registers BuildInitialContextRegisters() { @@ -172,6 +240,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/StackWalk.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/StackWalk.hpp new file mode 100644 index 0000000000..d7ae5782ca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/StackWalk.hpp @@ -0,0 +1,155 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_STACKWALK_HPP + +#include "prx/libc/include/exceptions/Unwind.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +class DwarfFrames { +public: + using Byte = LibcUnwind::Byte; + using Word = LibcUnwind::Word; + using Registers = std::array; + enum class Step { Stepped, NoRecord, Failed }; + + DwarfFrames(Word base, Word end, const Byte* frames, const Byte* framesEnd) : base(base), end(end) { + for (const Byte* p = frames; p != nullptr && framesEnd - p >= 8;) { + const Byte* record = p; + const auto length = LibcUnwind::Read(p); + if (length == 0) continue; + if (length == 0xffffffff || length < 4 || static_cast(framesEnd - p) < length) break; + const Byte* next = p + length; + if (LibcUnwind::Read(p) != 0) { + LibcUnwind::Frame frame{}; + const Byte* cursor = nullptr; + const char* augmentation = nullptr; + Byte lsdaEncoding = 0; + if (LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{0, record}, cursor, augmentation, lsdaEncoding)) index.emplace_back(frame.start, record); + } + p = next; + } + std::sort(index.begin(), index.end()); + } + + Word Base() const { + return base; + } + + bool Contains(Word pc) const { + return pc >= base && pc < end; + } + + bool Covers(Word pc) const { + LibcUnwind::Frame frame{}; + return find(pc, frame) == Step::Stepped; + } + + template + Step Unwind(Registers& registers, bool leaf, TRead&& read) const { + const Word pc = registers[16] - (leaf ? 0 : 1); + LibcUnwind::Frame frame{}; + if (const auto found = find(pc, frame); found != Step::Stepped) return found; + LibcUnwind::Rules initial; + if (!LibcUnwind::Instructions(frame.cieBegin, frame.cieEnd, frame, ~Word(0), initial, {})) return Step::Failed; + if constexpr (LibcUnwind::RuleCount > 32) { + if (frame.returnRegister == 32) { + initial.registers[16] = initial.registers[32]; + initial.registers[32] = {}; + frame.returnRegister = 16; + } + } + LibcUnwind::Rules rules = initial; + if (!LibcUnwind::Instructions(frame.begin, frame.end, frame, pc, rules, initial)) return Step::Failed; + if (rules.cfaExpression != nullptr || rules.cfaRegister >= 16 || frame.returnRegister >= 17) return Step::Failed; + const Word cfa = registers[rules.cfaRegister] + rules.cfaOffset; + if (cfa <= registers[7]) return Step::Failed; + Registers next = registers; + for (unsigned column = 0; column < next.size(); ++column) { + const auto& rule = rules.registers[column]; + if (rule.kind == 0) continue; + if (rule.kind == 1) { + if (!read(cfa + rule.value, next[column])) return Step::Failed; + } else if (rule.kind == 2 && rule.value >= 0 && rule.value < 17) { + next[column] = registers[rule.value]; + } else if (rule.kind == 4) { + next[column] = cfa + rule.value; + } else if (rule.kind == 5) { + next[column] = 0; + } else { + return Step::Failed; + } + } + next[7] = cfa; + next[16] = next[frame.returnRegister]; + if (next[16] == 0 || (next[7] == registers[7] && next[16] == registers[16])) return Step::Failed; + registers = next; + return Step::Stepped; + } + +private: + Step find(Word pc, LibcUnwind::Frame& frame) const { + const auto found = std::upper_bound(index.begin(), index.end(), pc, [](Word value, const auto& entry) { return value < entry.first; }); + if (found == index.begin()) return Step::NoRecord; + const Byte* p = nullptr; + const char* augmentation = nullptr; + Byte lsdaEncoding = 0; + if (!LibcUnwind::DecodeRange(frame, LibcUnwind::Lookup{pc, std::prev(found)->second}, p, augmentation, lsdaEncoding)) return Step::Failed; + if (pc < frame.start || pc - frame.start >= frame.length) return Step::NoRecord; + if (*augmentation == 'z') { + const auto size = LibcUnwind::Uleb(p); + p += size; + } + frame.begin = p; + return Step::Stepped; + } + + Word base; + Word end; + std::vector> index; +}; + +class CollapsedStacks { +public: + void Add(std::span leafFirst, bool truncated) { + std::vector stack(leafFirst.begin(), leafFirst.end()); + if (truncated) { + stack.push_back(0); + ++truncatedStacks; + } + ++stacks[std::move(stack)]; + } + + std::uint64_t Truncated() const { + return truncatedStacks; + } + + template + std::string Format(TName&& name) const { + std::string output; + for (const auto& [stack, count] : stacks) { + for (auto frame = stack.rbegin(); frame != stack.rend(); ++frame) { + if (frame != stack.rbegin()) output += ';'; + output += *frame == 0 ? std::string("[truncated]") : name(*frame); + } + output += ' ' + std::to_string(count) + '\n'; + } + return output; + } + +private: + std::map, std::uint64_t> stacks; + std::uint64_t truncatedStacks = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp new file mode 100644 index 0000000000..9174bfbc06 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TraceFile.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACEFILE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACEFILE_HPP + +#include +#include + +namespace AgcDriver { + +class TraceFile { +public: + static void WriteGzip(const std::filesystem::path& path, std::string_view text); + static void AppendLine(const std::filesystem::path& path, std::string_view line); +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp new file mode 100644 index 0000000000..f763218099 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/TracyZone.hpp @@ -0,0 +1,81 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_TRACYZONE_HPP + +#ifdef TRACY_ENABLE +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace AgcDriver { + +class TracyZone { +public: + explicit TracyZone([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + context = ___tracy_emit_zone_begin(location(name), 1); +#endif + } + + TracyZone(const TracyZone&) = delete; + TracyZone& operator=(const TracyZone&) = delete; + + ~TracyZone() { + End(); + } + + void End() { +#ifdef TRACY_ENABLE + if (context.active == 0) return; + ___tracy_emit_zone_end(context); + context.active = 0; +#endif + } + + static void MarkFrame() { +#ifdef TRACY_ENABLE + ___tracy_emit_frame_mark(nullptr); +#endif + } + + static void NameThread([[maybe_unused]] const char* name) { +#ifdef TRACY_ENABLE + ___tracy_set_thread_name(name); +#endif + } + +private: +#ifdef TRACY_ENABLE + static const ___tracy_source_location_data* location(const char* name) { + static constexpr std::size_t capacity = 256; + static std::array, capacity> names{}; + static std::array<___tracy_source_location_data, capacity> locations{}; + static std::mutex mutex; + const auto start = static_cast(reinterpret_cast(name) >> 4); + for (std::size_t probe = 0; probe < capacity; ++probe) { + const auto index = (start + probe) % capacity; + const auto* known = names[index].load(std::memory_order_acquire); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + std::lock_guard lock(mutex); + known = names[index].load(std::memory_order_relaxed); + if (known == name) return &locations[index]; + if (known != nullptr) continue; + locations[index] = {name, name, "", 0, 0}; + names[index].store(name, std::memory_order_release); + return &locations[index]; + } + throw std::runtime_error("Tracy: too many zone names"); + } + + TracyCZoneCtx context; +#endif +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 2027874af6..bd48aed75c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -39,6 +40,8 @@ public: VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; std::string DeviceName() const; + std::string DriverInfo() const; + bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; std::optional GeometryLimits() const; const std::uint32_t* WorkGroupCountLimit() const; @@ -219,7 +222,7 @@ public: // blit-submit times after the flip packet; noted in-place reads checked // (APS5_FLIP_READ_CHECK=1) and found overwritten before the blit's fence. struct PresentStatistics { - std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten; + std::uint64_t presents, gpuUnread, batchesAheadOfBlit, batchesAfterFlip, readsChecked, readsOverwritten, acquireDrops; double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; }; static PresentStatistics PresentCounts(); 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 cc300de66e..a6da3ceb55 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -75,6 +75,9 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; } else { if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); + if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { + if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); + } if (!partialLastGroup && fillBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice)) { pendingDispatchPhases().outcome = DispatchOutcome::FillHle; return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 1ca05db5ca..508bbbfcb1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -179,6 +179,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto label = Pm4::DecodeLabelWrite(packet); if (!label) break; NoteLabel(labels, label->address, label->Bytes()); + writes.emplace_back(label->address, label->address + label->Bytes().size()); } else if (opcode != 0x3f && opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (Pm4::AccessesMemory(header)) break; try { 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 e8ecb282d6..e51868b6aa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -145,6 +145,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 (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); 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..31803fd0f9 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; 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 d6d026478e..b4f9d0cd50 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -20,7 +20,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st program.binary, {waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)}, localDevice->Target(), - {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, + {0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}} }; const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0; @@ -83,7 +83,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st } 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 2af5a114da..80b56997d2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -115,6 +115,8 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; 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; 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 7b959293e6..069e72aeb1 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(value)); mix(pixel.dualSourceBlend); mix(pixel.alphaToCoverage); + mix(static_cast(pixel.rasterPrimitive)); 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 6dec41300d..8369224ac7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" #include #include #include @@ -6,36 +7,69 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool runtimeRegister(std::uint32_t offset) { + return offset - 0x00cu < 32u || offset - 0x08cu < 32u || offset - 0x10cu < 32u || offset == 0x082u || offset == 0x083u || offset == 0x102u || offset == 0x103u; +} + +const auto& registerMasks() { + static const auto masks = [] { + std::array, 3> bits; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = bits[static_cast(range.bank)]; + bank.resize(std::max(bank.size(), (static_cast(range.first) + range.count + 63) / 64)); + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + if (range.bank == Graphics::RegisterBank::Shader && runtimeRegister(offset)) continue; + bank[offset / 64] |= std::uint64_t{1} << (offset % 64); + } + } + std::array, 3> result; + for (std::size_t i = 0; i < bits.size(); ++i) result[i] = std::make_shared(std::move(bits[i])); + return result; + }(); + return masks; +} + +} + +std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental) { + const auto& masks = registerMasks(); + const auto fingerprint = [&](const Registers& registers, Graphics::RegisterBank bank) { + const auto& mask = masks[static_cast(bank)]; + return incremental ? registers.Fingerprint(mask) : registers.RecomputeFingerprint(*mask); + }; + auto shape = (0xcbf29ce484222325ull ^ fingerprint(queue.context, Graphics::RegisterBank::Context)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.shader, Graphics::RegisterBank::Shader)) * 0x100000001b3ull; + shape = (shape ^ fingerprint(queue.userConfig, Graphics::RegisterBank::UserConfig)) * 0x100000001b3ull; + auto exact = shape; + const auto words = [&](std::uint32_t base, std::uint32_t count) { + for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) { + exact = (exact ^ it->first) * 0x100000001b3ull; + exact = (exact ^ it->second) * 0x100000001b3ull; + } + }; + for (const auto base : {0x00cu, 0x08cu, 0x10cu}) { + const auto resources = queue.shader.find(base - 1); + const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + words(base, allUserWords ? 32u : std::min(count, 32u)); + } + words(0x082u, 2); + words(0x102u, 2); + return exact; +} + + std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) { static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; - std::uint64_t key = 0xcbf29ce484222325ull; + static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; + auto key = RegisterStateKey(queue, allUserWords); + if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); const auto mix = [&](std::uint64_t value) { key ^= value; key *= 0x100000001b3ull; }; - const auto userEnd = [&](std::uint32_t base) { - if (allUserWords) return base + 32u; - const auto resources = queue.shader.find(base - 1); - if (resources == queue.shader.end()) return base; - const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); - return base + std::min(count, 32u); - }; - const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; - const auto unread = [&](std::uint32_t offset) { - return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); - }; mix(deviceSerial); - for (const auto& range : Graphics::DrawKeyRegisters) { - const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; - mix((static_cast(range.bank) << 32u) | range.first); - const auto end = range.first + range.count; - const bool shader = range.bank == Graphics::RegisterBank::Shader; - for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { - if (shader && unread(it->first)) continue; - mix(it->first); - mix(it->second); - } - } for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { const auto low = queue.shader.find(base); const auto high = queue.shader.find(base + 1); @@ -57,7 +91,7 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis } bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) { - if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded) return false; + if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false; for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) { if (a.resources[i].fields != b.resources[i].fields) return false; const auto& x = a.resourcesDst[i]; @@ -84,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; + 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; }; 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; @@ -105,7 +139,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { 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 (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index fa645dd135..7928206823 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -3,13 +3,68 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include +#include #include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif namespace AgcDriver::DriverDetail { +namespace { + +constexpr std::uintptr_t InlineImportStackBytes = 256 * 1024; + +std::atomic gpuMapImportThreads{0}; + +bool StackHoldsImport() { +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); +#else + struct Bounds { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + Bounds() { + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) != 0) return; + void* base = nullptr; + std::size_t size = 0; + if (pthread_attr_getstack(&attributes, &base, &size) == 0 && base != nullptr) { + low = reinterpret_cast(base); + high = low + size; + } + pthread_attr_destroy(&attributes); + } + }; + thread_local const Bounds bounds; + const auto low = bounds.low; + const auto high = bounds.high; +#endif + const volatile unsigned char marker = 0; + const auto frame = reinterpret_cast(&marker); + return frame >= low && frame < high && frame - low >= InlineImportStackBytes; +} + +} + +std::uint64_t Driver::GpuMapImportThreads() { + return gpuMapImportThreads.load(std::memory_order_relaxed); +} + Driver& Driver::Get() { static Driver driver; return driver; @@ -50,6 +105,7 @@ void Driver::stop() { device.Reset(); replacedDevices.clear(); Graphics::ShutdownGuestBufferWorkers(); + PerformanceTrace::StopOutput(); StopProfileOutput_nid_no_patch(); stopped = true; } @@ -64,14 +120,29 @@ Driver::Driver() { throw; } GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix([](const GuestAllocations::Mapped& ranges, std::uint64_t generation) { - try { - const auto current = Driver::Get().device.Load(); - if (current == nullptr || current->ImportGuestMemory(ranges, generation, false)) return; - std::lock_guard lock(GuestMemory::GpuMutex()); - if (Driver::Get().device.Load() == current) current->ImportGuestMemory(ranges, generation, true); - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: %s\n", error.what()); + const auto import = [&] { + try { + const auto current = Driver::Get().device.Load(); + if (current == nullptr || current->ImportGuestMemory(ranges, generation, false)) return; + std::lock_guard lock(GuestMemory::GpuMutex()); + if (Driver::Get().device.Load() == current) current->ImportGuestMemory(ranges, generation, true); + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: %s\n", error.what()); + } + }; + if (GuestMemory::GpuMutex().HeldByThisThread() || StackHoldsImport()) { + import(); + return; } + std::thread host; + try { + host = std::thread(import); + } catch (const std::system_error& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: no host thread to run it on: %s\n", error.what()); + return; + } + gpuMapImportThreads.fetch_add(1, std::memory_order_relaxed); + host.join(); }); } 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 d947ec51e9..4d5f80f1f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -60,6 +60,24 @@ std::optional MatchDwordPatternFill(std::span MatchZeroDwordFill(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() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt; + static constexpr std::array kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + const auto stride = (userData[1] >> 16u) & 0x3fffu; + const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto outOfBounds = (userData[3] >> 28u) & 3u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto type = userData[3] >> 30u; + if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt; + const auto dwords = std::min(static_cast(packet[1]) * 64u, userData[2]); + const auto base = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt; + return DwordPatternFill{base, static_cast(dwords * 4u), {}}; +} + bool Driver::fillClearEnabled() { static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr; return enabled; @@ -125,7 +143,8 @@ 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) { - const auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute); + auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute); + if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute); if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false; std::uint64_t base; std::size_t bytes; 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 f462b90f80..c65419b0ee 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -205,6 +205,7 @@ void Driver::execute(const Submission& submission) { } else if (header == FlipPacketHeader) { CheckFailure(); std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0; + bool flipInterruptDeferred = false; if (!drains) { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); @@ -212,8 +213,13 @@ void Driver::execute(const Submission& submission) { const auto localDevice = device.Load(); if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); - if (localDevice != nullptr) localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); + if (localDevice != nullptr) { + localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); + const auto queueId = submission.queue; + flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false); + } } + if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index e1c00238f5..55701d83cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include namespace AgcDriver::DriverDetail { @@ -37,7 +38,7 @@ void Driver::AttachWindow(const PresentationWindow& window) { } void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context) { - + if (PerformanceTrace::Configured()) PerformanceTrace::Present(window.width, window.height, device.Load().get()); PerformanceContext timingContext(window.timing.get()); PerformanceTimer timing("Driver.Present"); static thread_local bool pinned = false; @@ -127,6 +128,7 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff } gpuReady(context); timing.Mark("release_and_callback"); + TracyZone::MarkFrame(); if (profile) reportPresents(waitedMs, inFlight); CheckFailure(); } catch (const ProcessShutdown&) { @@ -152,10 +154,12 @@ void Driver::reportPresents(double waitedMs, std::size_t inFlight) { const auto counts = VulkanDevice::PresentCounts(); const auto presents = counts.presents - previous.presents; const double perPresent = presents != 0 ? 1.0 / static_cast(presents) : 0.0; + const auto dropped = counts.acquireDrops - previous.acquireDrops; + const double perFrame = presents + dropped != 0 ? 1.0 / static_cast(presents + dropped) : 0.0; const auto flips = flipsCounted.load() - previousFlips; const double perFlip = flips != 0 ? 1.0 / static_cast(flips) : 0.0; const auto serial = flipSerial.load(); - AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per present: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perPresent, (counts.gpuGapMs - previous.gpuGapMs) * perPresent, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); + AgcDriver::ProfilePrint_nid_no_patch( "[present] %llu presents over 10 s (%llu dropped: no free swapchain image; %zu may trail on the GPU); %s p50 %.2f ms, p90 %.2f ms, max %.2f ms over %zu; per frame, presented or dropped: GPU busy %.1f ms, idle gaps %.1f ms (%llu batches without a completion record); per flip: %.1f batches recorded (%.1f unsignaled at the flip); per present: %.1f batches ahead of the blit, %.1f after it; after the flip packet: last submit %.1f ms, blit submit %.1f ms; in-place reads overwritten before execution: %llu of %llu checked\n", static_cast(presents), static_cast(dropped), inFlight, inFlight != 0 ? "inflight_wait" : "render_fence_wait", percentile(0.5), percentile(0.9), waits.empty() ? 0.0 : waits.back(), waits.size(), (counts.gpuBusyMs - previous.gpuBusyMs) * perFrame, (counts.gpuGapMs - previous.gpuGapMs) * perFrame, static_cast(counts.gpuUnread - previous.gpuUnread), static_cast(serial - windowSerial) * perFlip, static_cast(flipBatchesUnsignaled.load() - previousUnsignaled) * perFlip, static_cast(counts.batchesAheadOfBlit - previous.batchesAheadOfBlit) * perPresent, static_cast(counts.batchesAfterFlip - previous.batchesAfterFlip) * perPresent, (counts.lastSubmitAfterFlipMs - previous.lastSubmitAfterFlipMs) * perPresent, (counts.blitSubmitAfterFlipMs - previous.blitSubmitAfterFlipMs) * perPresent, static_cast(counts.readsOverwritten - previous.readsOverwritten), static_cast(counts.readsChecked - previous.readsChecked)); previous = counts; previousFlips = flipsCounted.load(); previousUnsignaled = flipBatchesUnsignaled.load(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index d568f7b811..56db0c1f68 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -7,10 +7,17 @@ #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include +#include #include namespace AgcDriver::DriverDetail { +namespace { + +constexpr auto IdleResultsQuietPeriod = std::chrono::milliseconds(50); + +} + FrameTiming* Driver::frameTiming() { if (!APS5_ENABLE_TIMING_LOG) return nullptr; if (pendingFrameTiming == nullptr) pendingFrameTiming = std::make_shared(++frameSerial, true); @@ -67,11 +74,9 @@ void Driver::reapCompletionLabels() { void Driver::run(std::uint32_t id) noexcept { onWorkerThread() = true; - { - char role[32]; - std::snprintf(role, sizeof role, "queue worker 0x%x", id); - PinWorkerThread(role); - } + char role[32]; + std::snprintf(role, sizeof role, "queue worker 0x%x", id); + PinWorkerThread(role); GuestMemory::TagGpuLockThread(id); if (id == 0) StartWorkerSampler(); @@ -79,6 +84,7 @@ void Driver::run(std::uint32_t id) noexcept { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& costs = submissionCosts(id); try { + if (PerformanceTrace::Configured()) PerformanceTrace::BindThread(role); for (;;) { submission = Submission{}; struct Running { @@ -114,6 +120,9 @@ void Driver::run(std::uint32_t id) noexcept { return runningWorkers.load(std::memory_order_acquire) == 0 && std::all_of(workers.begin(), workers.end(), [](const auto& item) { return item.second.pending.empty(); }); }; + const auto idleResultsPending = [&] { + return id == 0 && Graphics::StorageTexture::AnyPending() && allQueuesIdle() && !completionsPending(); + }; const auto poll = [&](const auto& done) { #ifdef _WIN32 lock.unlock(); @@ -128,7 +137,21 @@ void Driver::run(std::uint32_t id) noexcept { while (!ready()) { if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) { const auto idleEpochAtCheck = idleEpoch.load(std::memory_order_acquire); - const bool storeIdleResults = id == 0 && allQueuesIdle() && !completionsPending(); + const bool storeIdleResults = idleResultsPending(); + if (storeIdleResults) { + queue0Dormant.store(true, std::memory_order_relaxed); + const auto quietUntil = std::chrono::steady_clock::now() + IdleResultsQuietPeriod; + const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); + changed.wait_until(lock, quietUntil, [&] { + return ready() || idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait; + }); + queue0Dormant.store(false, std::memory_order_relaxed); + if (ready()) break; + if (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) { + continue; + } + if (!idleResultsPending()) continue; + } lock.unlock(); if (storeIdleResults) { if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp index d9d04c893a..0bac6c5eb9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" +#include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include "prx/libc/include/CpuTopology.hpp" #include @@ -20,6 +21,7 @@ std::uint64_t WorkerAffinityMask() { } void PinWorkerThread(const char* role) { + TracyZone::NameThread(role); const auto mask = WorkerAffinityMask(); if (mask == 0) return; static std::once_flag summary; 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 55c09e78c9..4ad3671f97 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -141,6 +141,7 @@ bool RawGraphicsDrawn() { namespace { void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock& lock) { + if (prepared.pending) PrioritizeRegistrationPreparation(prepared); prepared.settled.wait(lock, [&] { return !prepared.pending; }); if (prepared.failure != nullptr) std::rethrow_exception(prepared.failure); } @@ -167,6 +168,23 @@ 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) { + constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2; + const auto bits = paClVsOutCntl & Graphics::PositionExportLayoutBits; + if (bits == 0) return 0; + const bool cached = snapshot != nullptr && codeOffset == 0; + if (cached) { + const auto use = snapshot->positionVectorUse->load(std::memory_order_acquire); + if (use != Unknown) return use == Used ? bits : 0; + } + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code); + const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + return instruction.family == ShaderRecompiler::RdnaInstructionFamily::EXP && instruction.exportTarget >= 0x0du && instruction.exportTarget <= 0x0fu && instruction.exportEnableMask != 0; + }); + if (cached) snapshot->positionVectorUse->store(used ? Used : Unused, std::memory_order_release); + return used ? bits : 0; +} + bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) { Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5); @@ -299,6 +317,13 @@ bool FusedHullHeader(const ShaderSnapshot& snapshot, Shader used, const Shader& return true; } +static Shader GuestShaderFields(const Shader* shader) { + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader fields; + std::memcpy(&fields, static_cast(shader), sizeof(Shader)); + return fields; +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -312,7 +337,7 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { } std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { - const auto found = registry.find(reinterpret_cast(const_cast(shader->code))); + const auto found = registry.find(reinterpret_cast(const_cast(GuestShaderFields(shader).code))); if (found == registry.end()) return {}; for (const auto& snapshot : found->second) { if (snapshot->headerAddress == reinterpret_cast(shader)) return snapshot; @@ -374,6 +399,10 @@ void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler: APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize); return; } + if (request.context.vertex && (request.context.vertex->paClVsOutCntl & (1u << 18u)) != 0) { + APS5_LOG_OUT_DEBUG("Shader 0x%llx is drawn into a layered target; preparing its layered variant at draw", static_cast(snapshot.codeAddress)); + return; + } if (!snapshot.header.empty()) { if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast(request.shader.codeAddress), static_cast(request.shader.stage)); return; @@ -669,7 +698,11 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap if (firstUser == 0) userCount += 8; } plan->userData.resize(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) { + vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); + const auto outputs = state.context.find(0x207u); + if (vertex && outputs != state.context.end()) vertex->paClVsOutCntl = PositionExportLayout(&snapshot, codeOffset, std::span(snapshot.code).subspan(codeOffset), outputs->second); + } plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { @@ -681,6 +714,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap plan->memory = {{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; auto& request = plan->request.emplace(ShaderRecompiler::RecompileRequest{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, plan->userData, compute, pixel, vertex, plan->memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics}); if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes; + if (graphics && graphics->tessellation) request.layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes; if (pixel && device.GraphicsPipelineLibraries()) request.layout.pushConstantOffsetBytes = Graphics::FixedPushOffset(Stage::Fragment); return plan; } @@ -731,10 +765,18 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { - static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + static auto* pool = [] { + auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + PreparationSlot().store(created); + return created; + }(); return *pool; } + static RegistrationPreparePool* Existing() { + return PreparationSlot().load(); + } + static RegistrationPreparePool& Prebuild() { static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); @@ -750,6 +792,11 @@ public: } private: + static std::atomic& PreparationSlot() { + static std::atomic slot{nullptr}; + return slot; + } + static std::atomic& PrebuildSlot() { static std::atomic slot{nullptr}; return slot; @@ -757,11 +804,12 @@ private: public: - void Submit(std::function task) { + void Submit(std::function task, const PreparedShaders* prepared = nullptr) { { std::unique_lock lock(mutex); if (threads != 0 && !closed) { - tasks.push_back(std::move(task)); + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared}); + else tasks.push_back({std::move(task), prepared}); lock.unlock(); ready.notify_one(); return; @@ -770,6 +818,14 @@ public: task(); } + void Prioritize(const PreparedShaders& prepared) { + std::lock_guard lock(mutex); + const auto found = std::ranges::find_if(tasks, [&](const auto& task) { return task.prepared == &prepared; }); + if (found == tasks.end()) return; + if (std::getenv("APS5_TRACE_PREPARATION_WAIT") != nullptr) std::fprintf(stderr, "[shader-prepare] demanded queued task at position %zu of %zu\n", static_cast(std::distance(tasks.begin(), found)), tasks.size()); + std::rotate(tasks.begin(), found, std::next(found)); + } + void Close() { std::unique_lock lock(mutex); closed = true; @@ -793,8 +849,9 @@ private: static void* Main(void* argument) { auto& pool = *static_cast(argument); + if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8); for (;;) { - std::function task; + Task task; { std::unique_lock lock(pool.mutex); pool.ready.wait(lock, [&] { return !pool.tasks.empty(); }); @@ -802,8 +859,8 @@ private: pool.tasks.pop_front(); ++pool.running; } - task(); - task = nullptr; + task.function(); + task = {}; { std::lock_guard lock(pool.mutex); --pool.running; @@ -813,10 +870,15 @@ private: return nullptr; } + struct Task { + std::function function; + const PreparedShaders* prepared = nullptr; + }; + std::mutex mutex; std::condition_variable ready; std::condition_variable idle; - std::deque> tasks; + std::deque tasks; unsigned threads = 0; unsigned running = 0; bool closed = false; @@ -828,6 +890,7 @@ bool AsyncRegistrationPrepare() { } void PrepareRegisteredInBackground(std::shared_ptr snapshot, std::shared_ptr plan) { + const auto* preparedKey = snapshot->prepared.get(); SubmitRegistrationPreparation([snapshot = std::move(snapshot), plan = std::move(plan)] { PerformanceContext timingContext(FrameTiming::Preparation()); std::vector entries; @@ -849,13 +912,18 @@ void PrepareRegisteredInBackground(std::shared_ptr snapsho prepared.pending = false; } prepared.settled.notify_all(); - }); + }, preparedKey); } } -void SubmitRegistrationPreparation(std::function task) { - RegistrationPreparePool::Instance().Submit(std::move(task)); +void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared) { + RegistrationPreparePool::Instance().Submit(std::move(task), prepared); +} + +void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { + prepared.demanded.store(true); + if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); } void SubmitLibraryPrebuild(std::function task) { @@ -894,7 +962,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod std::vector prepared; std::uint32_t pushOffset = 0; const bool fixedSlots = target.fixedPushSlots; - const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushSlotBytes; + const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : decoded.state.stages.tessellation ? ShaderRecompiler::TessellationLevelPushOffsetBytes : Graphics::PipelinePushSlotBytes; for (std::size_t index = 0; index < decoded.programs.size(); ++index) { if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue; const auto& program = decoded.programs[index]; @@ -902,6 +970,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; pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; @@ -949,8 +1018,8 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std std::lock_guard lock(mutex); registry = shaders; for (const auto* shader : stages) { - GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader)); - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto fields = GuestShaderFields(shader); + const auto address = reinterpret_cast(const_cast(fields.code)); require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader"); const auto selected = RegisteredHeader(*registry, shader); require(selected != nullptr, "graphics ABI refers to a replaced shader header"); @@ -961,10 +1030,10 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std registered.shader.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.shader.insert_or_assign(offset, value); }); registered.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.context.insert_or_assign(offset, value); }); registered.userConfig.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.userConfig.insert_or_assign(offset, value); }); - if (shader->type != snapshot.type) { - const std::size_t count = shader->num_sh_registers; - GuestMemory::CheckRange(shader->sh_registers, count * sizeof(ShaderRegister), alignof(ShaderRegister)); - for (std::size_t index = 0; index < count; ++index) state.shader.insert_or_assign(shader->sh_registers[index].offset, shader->sh_registers[index].value); + if (fields.type != snapshot.type) { + const std::size_t count = fields.num_sh_registers; + GuestMemory::CheckRange(fields.sh_registers, count * sizeof(ShaderRegister), alignof(ShaderRegister)); + for (std::size_t index = 0; index < count; ++index) state.shader.insert_or_assign(fields.sh_registers[index].offset, fields.sh_registers[index].value); } } } @@ -1017,8 +1086,8 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::spantype != 0 && shader->type != 1) { + const auto fields = GuestShaderFields(shader); + if (fields.type != 0 && fields.type != 1) { const std::array stages{shader}; ResolveGraphicsStagesAbi(stages, context, primitive); transaction.Commit(); @@ -1027,7 +1096,7 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span snapshot; { std::lock_guard lock(mutex); - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto address = reinterpret_cast(const_cast(fields.code)); require(shaders != nullptr && shaders->contains(address), "static ABI refers to an unregistered shader"); snapshot = RegisteredHeader(*shaders, shader); require(snapshot != nullptr, "static ABI refers to a replaced shader header"); @@ -1094,10 +1163,8 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: std::shared_ptr fragment; { std::lock_guard lock(mutex); - GuestMemory::CheckRange(vertex, sizeof(Shader), alignof(Shader)); - GuestMemory::CheckRange(pixel, sizeof(Shader), alignof(Shader)); const auto lookup = [&](const Shader* shader) { - const auto address = reinterpret_cast(const_cast(shader->code)); + const auto address = reinterpret_cast(const_cast(GuestShaderFields(shader).code)); require(shaders != nullptr && shaders->contains(address), "rectangle ABI refers to an unregistered shader"); const auto snapshot = RegisteredHeader(*shaders, shader); require(snapshot != nullptr, "rectangle ABI refers to a replaced shader header"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 20ac655e17..f19c67b284 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -721,6 +722,15 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes; enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow, Fresh }; +struct WindowCopy { + std::uint64_t first; + std::span bytes; + + bool Holds(std::uintptr_t page) const { + return std::memcmp(reinterpret_cast(page), bytes.data() + (page - first), WritePageBytes) == 0; + } +}; + #ifdef _WIN32 void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation); #endif @@ -738,7 +748,6 @@ struct WriteTracker { // late label rule asks whether the CPU touched a label's block, and the driver's own GPU label // records in the same block (the title's per-job slots are 0x20 apart) must not count. std::vector cpuBlocks; - std::vector writtenBlocks; WriteWatchCoverage coverage; #else static constexpr std::size_t LeafBlocks = std::size_t{1} << 16; @@ -746,7 +755,6 @@ struct WriteTracker { struct Leaf { std::array blocks{}; std::array cpuBlocks{}; - std::array writtenBlocks{}; }; std::vector> leaves; #endif @@ -782,7 +790,6 @@ struct WriteTracker { if (!watched) return; blocks.assign(size / WriteBlockBytes + 1, 0); cpuBlocks.assign(size / WriteBlockBytes + 1, 0); - writtenBlocks.assign(size / WriteBlockBytes + 1, 0); coverage.Initialize(base, size); pages.resize(1u << 16); GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); @@ -935,28 +942,17 @@ struct WriteTracker { #endif } - std::uint32_t writtenStampOf(std::uint64_t block) const { -#ifdef _WIN32 - return writtenBlocks[block]; -#else - const auto& leaf = leaves[block / LeafBlocks]; - return leaf != nullptr ? leaf->writtenBlocks[block % LeafBlocks] : 0; -#endif - } - void stamp(std::uint64_t block, std::uint32_t stampGeneration, StampKind kind) { const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow; - const bool written = kind != StampKind::ImportWindow; + const bool stored = kind != StampKind::ImportWindow; #ifdef _WIN32 - blocks[block] = stampGeneration; + if (stored) blocks[block] = stampGeneration; if (cpu) cpuBlocks[block] = stampGeneration; - if (written) writtenBlocks[block] = stampGeneration; #else auto& leaf = leaves[block / LeafBlocks]; if (leaf == nullptr) leaf = std::make_unique(); - leaf->blocks[block % LeafBlocks] = stampGeneration; + if (stored) leaf->blocks[block % LeafBlocks] = stampGeneration; if (cpu) leaf->cpuBlocks[block % LeafBlocks] = stampGeneration; - if (written) leaf->writtenBlocks[block % LeafBlocks] = stampGeneration; #endif } }; @@ -1029,11 +1025,11 @@ bool sharedCollectMemo() { return shared; } -#ifndef _WIN32 struct StampRuns { WriteTracker& tracker; StampKind kind; std::size_t stamped = 0; + const WindowCopy* window = nullptr; }; void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { @@ -1044,60 +1040,49 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) { ++runs.stamped; const auto generation = tracker.generation.load(std::memory_order_relaxed); if (kind != StampKind::Driver) tracker.noteCpuStore(begin, end, generation); - for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind); + if (runs.window != nullptr) { + for (auto page = begin; page < end; page += WritePageBytes) tracker.stamp(tracker.blockOf(page), generation, runs.window->Holds(page) ? StampKind::ImportWindow : kind); + } else { + for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind); + } +#ifndef _WIN32 collectDirtyRuns.fetch_add(1, std::memory_order_relaxed); +#endif +} + +#ifdef _WIN32 +bool collectWindowsWrites(StampRuns& runs, std::uint64_t first, std::uint64_t stop, bool armedOnly) { + auto& tracker = runs.tracker; + const bool fresh = runs.kind == StampKind::Fresh; + const auto collect = fresh ? &GuestArena::GuestArenaCollectFreshWrites_nid_postfix : armedOnly ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix; + static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; + const auto walk = [&](bool clear) { + auto cursor = first; + while (cursor < stop) { + std::size_t count = tracker.pages.size(); + if (!collect(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, clear)) return false; + for (std::size_t i = 0; i < count; ++i) { + const auto page = reinterpret_cast(tracker.pages[i]); + stampWrittenRun(&runs, page, page + WritePageBytes); + } + if (count < tracker.pages.size()) break; + cursor = reinterpret_cast(tracker.pages[count - 1]) + WritePageBytes; + } + return true; + }; + if (!walk(singlePass || fresh)) return false; + if (runs.stamped != 0) collectDirty.fetch_add(1, std::memory_order_relaxed); + return fresh || singlePass || runs.stamped == 0 || walk(true); } #endif -bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, StampKind kind, bool armedOnly = false) { +bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, StampKind kind, bool armedOnly = false, const WindowCopy* window = nullptr) { ++tracker.generation; + StampRuns runs{tracker, kind, 0, window}; #ifdef _WIN32 - static_cast(armedOnly); - constexpr std::uint64_t page = 4096; - // One resetting walk: the kernel reports and clears a page's dirty bit together, a write landing - // after the walk passed a page is reported by the next walk, and a clean page is not touched by the - // reset, so a probe pass first (which doubled the walk of every dirty range) buys nothing. - // APS5_NO_SINGLE_PASS_COLLECT=1 restores the probe pass followed by a resetting pass when dirty. - static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; - auto cursor = first; - bool dirty = false; - while (cursor < stop) { - std::size_t count = tracker.pages.size(); - DWORD granularity = 4096; - if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, singlePass)) { - // Uncommitted pages inside the range make the call fail; such ranges are compared instead. - return false; - } - if (count != 0) dirty = true; - for (ULONG_PTR i = 0; i < count; ++i) { - const auto page = reinterpret_cast(tracker.pages[i]); - tracker.stamp(tracker.blockOf(page), tracker.generation, kind); - if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation); - } - if (count < tracker.pages.size()) break; - cursor = reinterpret_cast(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page); - } - if (dirty) collectDirty.fetch_add(1, std::memory_order_relaxed); - if (dirty && !singlePass) { - // The resetting pass reports pages again, so a write racing the first pass is stamped too. - cursor = first; - while (cursor < stop) { - std::size_t count = tracker.pages.size(); - DWORD granularity = 4096; - if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, true)) return false; - for (ULONG_PTR i = 0; i < count; ++i) { - const auto page = reinterpret_cast(tracker.pages[i]); - tracker.stamp(tracker.blockOf(page), tracker.generation, kind); - if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation); - } - if (count < tracker.pages.size()) break; - cursor = reinterpret_cast(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page); - } - } - return true; + return collectWindowsWrites(runs, first, stop, armedOnly); #else const auto dirtyRuns = collectDirtyRuns.load(std::memory_order_relaxed); - StampRuns runs{tracker, kind}; const auto collect = armedOnly ? &GuestWriteWatch::GuestWriteWatchCollectArmed_nid_postfix : &GuestWriteWatch::GuestWriteWatchCollect_nid_postfix; const bool complete = collect(static_cast(first), static_cast(stop - first), &stampWrittenRun, &runs); if (collectDirtyRuns.load(std::memory_order_relaxed) != dirtyRuns) collectDirty.fetch_add(1, std::memory_order_relaxed); @@ -1207,7 +1192,7 @@ void Unwatch(std::uint64_t address, std::size_t bytes) { unwatchLocked(tracker, address, bytes); } -bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import) { +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(); auto& tracker = Tracker(); auto lock = lockTracker(tracker); @@ -1220,9 +1205,21 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function const auto first = address & ~(page - 1); const auto stop = (address + bytes + page - 1) & ~(page - 1); const bool covered = tracker.covers(first, static_cast(stop - first)); + std::vector saved; + if (covered && staysWatched) { + CheckRange(reinterpret_cast(first), static_cast(stop - first), 1, false); + const auto* source = reinterpret_cast(first); + saved.assign(source, source + (stop - first)); + } const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true); if (!import()) return false; - if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); + if (!staysWatched) { + if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast(stop - first)); + unwatchLocked(tracker, address, bytes); + return true; + } + const WindowCopy window{first, saved}; + if (!before || !walkWrites(tracker, first, stop, StampKind::Cpu, true, &window)) unwatchLocked(tracker, first, static_cast(stop - first)); return true; } @@ -1320,19 +1317,22 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { tracker.noteUnstampedStore(address, address + bytes); return 0; } -#ifndef _WIN32 { const auto blockFirst = address & ~(std::uint64_t{WriteBlockBytes} - 1); const auto blockStop = (address + bytes + WriteBlockBytes - 1) & ~(std::uint64_t{WriteBlockBytes} - 1); StampRuns runs{tracker, StampKind::Fresh}; - if (!GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs)) { +#ifdef _WIN32 + const bool complete = collectWindowsWrites(runs, blockFirst, blockStop, false); +#else + const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); +#endif + if (!complete) { unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); tracker.noteUnstampedStore(address, address + bytes); return 0; } if (runs.stamped != 0) ++tracker.generation; } -#endif const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { @@ -1354,7 +1354,7 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(end - 1); for (auto block = first; block <= last; ++block) { - if (tracker.writtenStampOf(block) <= generation) continue; + if (tracker.stampOf(block) <= generation) continue; if (const auto found = tracker.cpuPages.find(block); found != tracker.cpuPages.end()) { const auto begin = tracker.blockBegin(block); const auto from = std::max(address, begin) - begin; @@ -1425,7 +1425,7 @@ bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t genera const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); for (auto block = first; block <= last; ++block) { - if (tracker.writtenStampOf(block) > generation) return true; + if (tracker.stampOf(block) > generation) return true; } return false; } @@ -1443,7 +1443,7 @@ bool ChangedBlocks(std::uint64_t address, std::size_t bytes, std::span= generations.size() || k >= changed.size()) break; const auto generation = generations[k]; - changed[k] = generation == 0 || tracker.writtenStampOf(block) > generation ? BlockWritten : tracker.stampOf(block) > generation ? BlockMaybeWritten : BlockUnchanged; + changed[k] = generation == 0 || tracker.stampOf(block) > generation ? BlockWritten : BlockUnchanged; if (k < cpu.size()) cpu[k] = generation == 0 || tracker.cpuStampOf(block) > generation ? 1 : 0; } return true; @@ -1671,7 +1671,17 @@ void AssertGpuLockHeld(const char* where) { void GpuMutexType::lock() { if (!GpuLockProfiled()) { - mutex.lock(); + auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr; + if (trace == nullptr) { + mutex.lock(); + } else { + const auto site = static_cast(std::exchange(LockStats().nextSite, GpuLockSite::Other)); + if (!mutex.try_lock()) { + const auto start = std::chrono::steady_clock::now(); + mutex.lock(); + trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now()); + } + } acquired(); return; } @@ -1689,6 +1699,7 @@ void GpuMutexType::lock() { mutex.lock(); acquired(); const auto now = std::chrono::steady_clock::now(); + if (auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr) trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, now); if (holders) { HolderMatrixValues after{}; Holders().Snapshot(after); @@ -1762,7 +1773,7 @@ void GpuMutexType::lock() { void TagGpuLockSite(GpuLockSite site) { // Off the profiled path the tag is never read, so the thread-local is not touched either. - if (!GpuLockProfiled()) return; + if (!GpuLockProfiled() && !PerformanceTrace::Recording()) return; LockStats().nextSite = site; } @@ -1834,12 +1845,9 @@ void Read(std::uint64_t address, std::span destination, std::size_t a std::memcpy(destination.data(), source, destination.size()); } -void ReadCommitted(std::uint64_t address, std::span destination) { - if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); - FlushGpuWrites(address, destination.size()); - const TimedAccess timed(CounterRead, destination.size()); - CountReadCaller(__builtin_return_address(0)); +namespace { + +void copyCommitted(std::uint64_t address, std::span destination) { if (Accessible(reinterpret_cast(address), destination.size())) { std::memcpy(destination.data(), reinterpret_cast(address), destination.size()); return; @@ -1848,6 +1856,25 @@ void ReadCommitted(std::uint64_t address, std::span destination) { for (const auto& [begin, end] : CommittedRanges(address, destination.size())) std::memcpy(destination.data() + (begin - address), reinterpret_cast(begin), static_cast(end - begin)); } +} + +void ReadCommitted(std::uint64_t address, std::span destination) { + if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + FlushGpuWrites(address, destination.size()); + const TimedAccess timed(CounterRead, destination.size()); + CountReadCaller(__builtin_return_address(0)); + copyCommitted(address, destination); +} + +void ReadCommittedUnsynced(std::uint64_t address, std::span destination) { + if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + const TimedAccess timed(CounterRead, destination.size()); + CountReadCaller(__builtin_return_address(0)); + copyCommitted(address, destination); +} + bool EqualsCommitted(std::uint64_t address, std::span bytes) { if (bytes.empty()) return true; FlushGpuWrites(address, bytes.size()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp new file mode 100644 index 0000000000..d2320c3f5c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -0,0 +1,189 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/TraceRevision.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +#ifndef _WIN32 +extern char** environ; +#endif + +namespace AgcDriver { + +namespace { + +using Clock = std::chrono::steady_clock; +constexpr std::size_t GpuRangeCapacity = TraceThread::DefaultCapacity; + +struct Session { + std::mutex mutex; + std::vector> threads; + std::optional window; + Clock::time_point first{}; + std::uint64_t present = 0; + std::uint64_t firstPresent = 0; + double gpuBusyAtOpen = 0; + std::optional gpuClock; + std::vector presents; +}; + +Session& session() { + static auto& state = *new Session(); + return state; +} + +std::unique_ptr& writer() { + static std::unique_ptr output; + return output; +} + +std::uint64_t processId() { +#ifdef _WIN32 + return GetCurrentProcessId(); +#else + return static_cast(::getpid()); +#endif +} + +std::string operatingSystem() { +#ifdef _WIN32 + using QueryVersion = LONG(WINAPI*)(PRTL_OSVERSIONINFOW); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"))); + RTL_OSVERSIONINFOW version{}; + version.dwOSVersionInfoSize = sizeof(version); + if (query == nullptr || query(&version) != 0) throw std::runtime_error("RtlGetVersion failed"); + return "Windows " + std::to_string(version.dwMajorVersion) + "." + std::to_string(version.dwMinorVersion) + "." + std::to_string(version.dwBuildNumber); +#else + utsname name{}; + if (::uname(&name) != 0) throw std::system_error(errno, std::generic_category(), "uname"); + return std::string(name.sysname) + " " + name.release; +#endif +} + +std::vector> environment() { +#ifdef _WIN32 + char** entries = _environ; +#else + char** entries = environ; +#endif + std::vector> result; + for (; entries != nullptr && *entries != nullptr; ++entries) { + const std::string_view entry = *entries; + const auto equals = entry.find('='); + if (!entry.starts_with("APS5_") || equals == std::string_view::npos) continue; + std::string value(entry.substr(equals + 1)); + if (value.find_first_of("/\\") != std::string::npos) value = ""; + result.emplace_back(std::string(entry.substr(0, equals)), std::move(value)); + } + std::sort(result.begin(), result.end()); + return result; +} + +} + +void PerformanceTrace::BindThread(const char* name, std::size_t capacity) { + if (!Configured() || local != nullptr) return; + auto bound = std::make_unique(name, presentCount, capacity); + auto& state = session(); + std::lock_guard lock(state.mutex); + local = bound.get(); + state.threads.push_back(std::move(bound)); +} + +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()); + state.first = now; + } + const auto present = state.present++; + const auto edge = state.window->Present(present, now - state.first); + if (edge == TraceWindow::Edge::Open) { + state.firstPresent = present; + state.gpuBusyAtOpen = VulkanDevice::PresentCounts().gpuBusyMs; + TraceClockSample clock; + if (device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) state.gpuClock = clock; + { + std::lock_guard gpu(gpuMutex); + gpuRanges.clear(); + gpuRanges.reserve(GpuRangeCapacity); + } + state.presents.push_back(now); + presentCount.fetch_add(1, 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; + 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); + + std::vector snapshots; + std::size_t presenter = 0; + 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()); + } + std::vector gpuSlices; + TraceClockSample clock; + if (state.gpuClock && device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) { + std::lock_guard gpu(gpuMutex); + gpuSlices = TraceFormat::GpuSlices(gpuRanges, *state.gpuClock, clock, state.presents.front()); + 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; + 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) { + try { + std::filesystem::create_directories(directory); + TraceFile::WriteGzip(directory / name, text); + TraceFile::AppendLine(directory / "aps5-summary.jsonl", line); + std::fprintf(stderr, "[trace] wrote %s and its line in aps5-summary.jsonl\n", name.c_str()); + std::fflush(stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what()); + std::fflush(stderr); + throw; + } + }, std::numeric_limits::max()); + ProfilePrint_nid_no_patch("[trace] closed at present %llu, writing %s (%zu bytes of JSON)\n", static_cast(present), summary.trace.c_str(), events.size()); + output->Write(std::move(events)); +} + +void PerformanceTrace::StopOutput() { + if (writer()) writer()->Stop(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp new file mode 100644 index 0000000000..40d0a7e089 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/TraceFile.cpp @@ -0,0 +1,61 @@ +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "zlib.h" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view text) { + z_stream stream{}; + if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) throw std::runtime_error("deflateInit2 failed"); + struct End { + z_stream& stream; + ~End() { deflateEnd(&stream); } + } end{stream}; + std::ofstream output; + output.exceptions(std::ios::badbit | std::ios::failbit); + output.open(path, std::ios::binary | std::ios::trunc); + const std::array header{0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff}; + output.write(reinterpret_cast(header.data()), header.size()); + uLong crc = crc32(0, Z_NULL, 0); + std::vector chunk(1 << 16); + std::size_t offset = 0; + int status = Z_OK; + while (status != Z_STREAM_END) { + if (stream.avail_in == 0 && offset < text.size()) { + const auto piece = static_cast(std::min(text.size() - offset, std::size_t{1} << 30)); + stream.next_in = reinterpret_cast(const_cast(text.data() + offset)); + stream.avail_in = piece; + crc = crc32(crc, stream.next_in, piece); + offset += piece; + } + stream.next_out = chunk.data(); + stream.avail_out = static_cast(chunk.size()); + status = deflate(&stream, offset == text.size() ? Z_FINISH : Z_NO_FLUSH); + if (status != Z_OK && status != Z_STREAM_END) throw std::runtime_error("deflate failed with " + std::to_string(status)); + output.write(reinterpret_cast(chunk.data()), static_cast(chunk.size() - stream.avail_out)); + } + std::array trailer{}; + const auto size = static_cast(text.size()); + for (unsigned index = 0; index < 4; ++index) { + trailer[index] = static_cast((crc >> (8 * index)) & 0xff); + trailer[4 + index] = static_cast((size >> (8 * index)) & 0xff); + } + output.write(reinterpret_cast(trailer.data()), trailer.size()); + output.close(); +} + +void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) { + std::ofstream output; + output.exceptions(std::ios::badbit | std::ios::failbit); + output.open(path, std::ios::binary | std::ios::app); + output.write(line.data(), static_cast(line.size())); + output.close(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8da0d00f9f..5d3ddfec4c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -255,6 +256,9 @@ struct VulkanDevice::State { void* uploadMapping = nullptr; VkDeviceSize uploadSize = 0; VkPhysicalDeviceProperties properties{}; + std::string driverInfo; + PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; + VkTimeDomainKHR hostTimeDomain = VK_TIME_DOMAIN_DEVICE_KHR; VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; std::vector capabilities{1}; std::vector spirvExtensions; @@ -265,9 +269,11 @@ struct VulkanDevice::State { bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; + bool shaderCullDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool float16DenormPreserve = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -278,6 +284,7 @@ struct VulkanDevice::State { VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT}; bool imageInt64Atomics = false; bool bufferInt64Atomics = false; + bool viewportIndexLayer = false; bool primitiveListRestart = false; bool depthClipControl = false; bool graphicsPipelineLibrary = false; @@ -512,7 +519,7 @@ struct VulkanDevice::State { // Whether a recorded dispatch's or draw's copied write-back (listed until it ran) stores into // the range on the CPU; under GpuMutex. bool CopiedWriterOverlaps(std::uint64_t address, std::size_t bytes) const { - const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; + const auto overlaps = [&](const auto& writer) { return writer->CopiedWritesOverlap(address, bytes); }; return std::any_of(copiedWriters->begin(), copiedWriters->end(), overlaps) || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), overlaps); } @@ -1039,6 +1046,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector availableExtensions(extensionCount); check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties"); const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { + VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); require(hasExtension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME), "VK_KHR_shader_float_controls is unavailable"); @@ -1138,6 +1151,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->imageInt64Atomics = true; state->spirvExtensions.push_back("SPV_EXT_shader_image_int64"); } + if (hasExtension(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME)) { + deviceExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME); + state->capabilities.push_back(spv::CapabilityShaderViewportIndexLayerEXT); + state->spirvExtensions.push_back("SPV_EXT_shader_viewport_index_layer"); + state->viewportIndexLayer = true; + } deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve); state->spirvExtensions.push_back("SPV_KHR_float_controls"); @@ -1156,6 +1175,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityRoundingModeRTE); state->capabilities.push_back(spv::CapabilityDenormPreserve); } + state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && + floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; + if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -1388,8 +1410,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; + enabled.shaderCullDistance = available.shaderCullDistance; state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + state->shaderCullDistance = enabled.shaderCullDistance == VK_TRUE; if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); + if (enabled.shaderCullDistance) state->capabilities.push_back(spv::CapabilityCullDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; @@ -1433,6 +1458,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->samplerAnisotropy = true; state->textureCompressionBC = true; deviceInfo.pEnabledFeatures = &enabled; + const char* calibratedTimestamps = !PerformanceTrace::Configured() ? nullptr : hasExtension(VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) ? VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME : hasExtension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) ? VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME : nullptr; + if (calibratedTimestamps != nullptr) deviceExtensions.push_back(calibratedTimestamps); deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); if (state->meshShader) { @@ -1584,6 +1611,25 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->shaderProfile = std::move(shaderProfile); if (compileOnly) return; + if (calibratedTimestamps != nullptr) { + const bool khr = std::strcmp(calibratedTimestamps, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) == 0; + const auto domains = reinterpret_cast(state->instanceProc(state->instance, khr ? "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR" : "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT")); + std::uint32_t domainCount = 0; + std::vector available; + if (domains != nullptr && domains(selected, &domainCount, nullptr) == VK_SUCCESS) { + available.resize(domainCount); + if (domains(selected, &domainCount, available.data()) != VK_SUCCESS) available.clear(); + } +#ifdef _WIN32 + const auto host = VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_KHR; +#else + const auto host = VK_TIME_DOMAIN_CLOCK_MONOTONIC_KHR; +#endif + if (std::ranges::find(available, VK_TIME_DOMAIN_DEVICE_KHR) != available.end() && std::ranges::find(available, host) != available.end()) { + state->hostTimeDomain = host; + state->calibratedTimestamps = state->DeviceFunction(khr ? "vkGetCalibratedTimestampsKHR" : "vkGetCalibratedTimestampsEXT"); + } + } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); 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}; @@ -1926,8 +1972,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa if (recorder.PendingWriteOverlaps(address, bytes)) { // Recorded draws with copied writes keep their own registry (Graphics::DrawCopiedWriters): // their CPU write-back would land after a fill recorded on the GPU, so they count too. - bool sync = alwaysSync || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }); + bool sync = alwaysSync || state->CopiedWriterOverlaps(address, bytes); if (!sync && Graphics::Recorder::PendingCompletionLabels() != 0) { // A label the CPU will store after a completion must not be overwritten by the fill: // only a label inside the filled range matters, whatever the range's size. @@ -2130,9 +2175,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st // A store a batch's completion makes on the CPU over the range (a copied buffer's write-back, // a completion-deferred label): it lands only when that batch is reaped, whatever its fence. const auto completionStoreOverlaps = [&](std::uint64_t address) { - if (alwaysSync || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); })) return true; - return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.PendingLabelIn(address, bytes); + if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true; + return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes); }; // The CPU decision (see the header): pure queries, in the order that makes the first failing // test the reason; a signaled writer of the source is fine (its in-place stores are final in @@ -2609,11 +2653,15 @@ bool VulkanDevice::AcquireImage() { std::uint32_t index = 0; timing.Mark("fence_reset"); // An out-of-date swapchain (the window changed) drops this frame; the next Resize recreates it. - const auto acquired = state->DeviceFunction("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index); + const auto acquired = state->DeviceFunction("vkAcquireNextImageKHR")(state->device, state->swapchain, 0, VK_NULL_HANDLE, state->acquireFence, &index); if (acquired == VK_ERROR_OUT_OF_DATE_KHR) { state->extent = {0, 0}; return false; } + if (acquired == VK_NOT_READY || acquired == VK_TIMEOUT) { + ++presentCounters.acquireDrops; + return false; + } if (acquired != VK_SUBOPTIMAL_KHR) check(acquired, "vkAcquireNextImageKHR"); timing.Mark("acquire_image"); APS5_LOG_OUT_DEBUG("vkAcquireNextImageKHR index=%u imageCount=%zu", index, state->images.size()); @@ -2942,6 +2990,34 @@ std::string VulkanDevice::DeviceName() const { return state->properties.deviceName; } +std::string VulkanDevice::DriverInfo() const { + return state->driverInfo.empty() ? std::to_string(state->properties.driverVersion) : state->driverInfo; +} + +bool VulkanDevice::CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const { + if (state->calibratedTimestamps == nullptr) return false; + const std::array domains{VkCalibratedTimestampInfoKHR{VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR, nullptr, VK_TIME_DOMAIN_DEVICE_KHR}, VkCalibratedTimestampInfoKHR{VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR, nullptr, state->hostTimeDomain}}; + std::array stamps{}; + std::uint64_t deviation = 0; + if (state->calibratedTimestamps(state->device, static_cast(domains.size()), domains.data(), stamps.data(), &deviation) != VK_SUCCESS) return false; +#ifdef _WIN32 + LARGE_INTEGER counter{}; + LARGE_INTEGER frequency{}; + QueryPerformanceCounter(&counter); + const auto now = std::chrono::steady_clock::now(); + QueryPerformanceFrequency(&frequency); + const double sinceNs = static_cast(static_cast(counter.QuadPart) - stamps[1]) * 1e9 / static_cast(frequency.QuadPart); +#else + timespec monotonic{}; + clock_gettime(CLOCK_MONOTONIC, &monotonic); + const auto now = std::chrono::steady_clock::now(); + const double sinceNs = static_cast(static_cast(monotonic.tv_sec) * 1000000000ull + static_cast(monotonic.tv_nsec) - stamps[1]); +#endif + gpuNs = static_cast(stamps[0]) * state->properties.limits.timestampPeriod; + cpu = now - std::chrono::duration_cast(std::chrono::duration(sinceNs)); + return true; +} + const std::uint32_t* VulkanDevice::WorkGroupCountLimit() const { return state->properties.limits.maxComputeWorkGroupCount; } @@ -2962,6 +3038,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.nonConstantImageOffsets = state->maintenance8; target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; + target.float16DenormPreserve = state->float16DenormPreserve; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; @@ -3071,9 +3148,11 @@ Graphics::Context VulkanDevice::buildContext() const { context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; context.shaderClipDistance = state->shaderClipDistance; + context.shaderCullDistance = state->shaderCullDistance; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); + context.viewportIndexLayer = state->viewportIndexLayer; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; @@ -3788,8 +3867,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out recorder.Sync(); } else if (record.shader->program->workgroupMemoryDwords != 0) { outcome.cpuReason = 8; - } else if (labelPending() || std::any_of(state->copiedWriters->begin(), state->copiedWriters->end(), [&](const auto& writer) { return writer->WritesOverlap(arguments, 12); }) - || std::any_of(Graphics::DrawCopiedWriters()->begin(), Graphics::DrawCopiedWriters()->end(), [&](const auto& writer) { return writer->WritesOverlap(arguments, 12); })) { + } else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) { outcome.cpuReason = 2; } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) { // Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index e48bdbbbc1..8b3ad92f49 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -3,9 +3,11 @@ #include #include #ifdef _WIN32 +#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp" #include #include #include +#include #include #include #include @@ -29,43 +31,134 @@ std::jthread workerSamplerThread; // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. struct Sampler { + static constexpr int ProcessDepth = 12; + static constexpr int WorkerDepth = 64; + HANDLE target = nullptr; std::string path; + int depth = ProcessDepth; + bool keepStacks = false; + std::uint64_t stackLow = 0; + std::uint64_t stackHigh = 0; + std::vector modules; + std::uint64_t pendingModule = 0; std::mutex mutex; std::map> counts; + CollapsedStacks stacks; std::uint64_t samples = 0; + std::int64_t suspendedTicks = 0; + std::int64_t longestSuspendedTicks = 0; ~Sampler() noexcept(false) { if (target != nullptr && !CloseHandle(target)) throw std::system_error(GetLastError(), std::system_category(), "Closing sampler thread handle"); } - void sample() { - CONTEXT context{}; - context.ContextFlags = CONTEXT_FULL; - if (SuspendThread(target) == static_cast(-1)) return; - const bool captured = GetThreadContext(target, &context) != 0; - std::vector frames; - if (captured) { - // Unwinding reads the target's stack while it is suspended; frames without unwind data - // (JIT/guest code) end the walk. - for (int depth = 0; depth < 12 && context.Rip != 0; ++depth) { - frames.push_back(context.Rip); - DWORD64 imageBase = 0; - auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr); - if (entry == nullptr) break; + bool readStack(std::uint64_t address, std::uint64_t& value) const { + if (address < stackLow || address > stackHigh - sizeof(value)) return false; + std::memcpy(&value, reinterpret_cast(address), sizeof(value)); + return true; + } + + const DwarfFrames* moduleOf(std::uint64_t pc) const { + const auto found = std::upper_bound(modules.begin(), modules.end(), pc, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); }); + return found != modules.begin() && std::prev(found)->Contains(pc) ? &*std::prev(found) : nullptr; + } + + bool hasUnwindData(std::uint64_t pc) const { + DWORD64 imageBase = 0; + const auto* module = moduleOf(pc - 1); + return RtlLookupFunctionEntry(pc, &imageBase, nullptr) != nullptr || (module != nullptr && module->Covers(pc - 1)); + } + + DwarfFrames::Step stepDwarf(CONTEXT& context, bool leaf) { + const auto* module = moduleOf(context.Rip - (leaf ? 0 : 1)); + if (module == nullptr) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(context.Rip), &memory, sizeof(memory)) != 0 && memory.Type == MEM_IMAGE) pendingModule = reinterpret_cast(memory.AllocationBase); + return DwarfFrames::Step::Failed; + } + DwarfFrames::Registers registers{context.Rax, context.Rdx, context.Rcx, context.Rbx, context.Rsi, context.Rdi, context.Rbp, context.Rsp, context.R8, context.R9, context.R10, context.R11, context.R12, context.R13, context.R14, context.R15, context.Rip}; + const auto step = module->Unwind(registers, leaf, [&](std::uint64_t address, std::uint64_t& value) { return readStack(address, value); }); + if (step != DwarfFrames::Step::Stepped) return step; + DWORD64* columns[] = {&context.Rax, &context.Rdx, &context.Rcx, &context.Rbx, &context.Rsi, &context.Rdi, &context.Rbp, &context.Rsp, &context.R8, &context.R9, &context.R10, &context.R11, &context.R12, &context.R13, &context.R14, &context.R15, &context.Rip}; + for (std::size_t column = 0; column < registers.size(); ++column) *columns[column] = registers[column]; + return step; + } + + int walk(CONTEXT& context, std::array& frames) { + const bool dwarf = stackHigh != 0 && context.Rsp >= stackLow && context.Rsp < stackHigh; + int count = 0; + while (count < depth && context.Rip != 0) { + frames[count++] = context.Rip; + DWORD64 imageBase = 0; + if (auto* entry = RtlLookupFunctionEntry(context.Rip, &imageBase, nullptr)) { PVOID handler = nullptr; DWORD64 establisher = 0; RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, context.Rip, entry, &context, &handler, &establisher, nullptr); + continue; } + if (!dwarf) break; + const auto step = stepDwarf(context, count == 1); + if (step == DwarfFrames::Step::Stepped) continue; + std::uint64_t returnAddress = 0; + if (step == DwarfFrames::Step::Failed || count != 1 || !readStack(context.Rsp, returnAddress) || !hasUnwindData(returnAddress)) break; + context.Rip = returnAddress; + context.Rsp += sizeof(returnAddress); } + return count; + } + + void indexModule() { + const auto base = std::exchange(pendingModule, 0); + if (moduleOf(base) != nullptr) return; + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const DwarfFrames::Byte* frames = nullptr; + const DwarfFrames::Byte* framesEnd = nullptr; + for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) { + if (std::memcmp(sections[index].Name, ".ehfram", 8) != 0) continue; + frames = reinterpret_cast(base + sections[index].VirtualAddress); + framesEnd = frames + sections[index].Misc.VirtualSize; + } + const auto at = std::upper_bound(modules.begin(), modules.end(), base, [](std::uint64_t value, const DwarfFrames& module) { return value < module.Base(); }); + modules.emplace(at, base, base + nt->OptionalHeader.SizeOfImage, frames, framesEnd); + } + + void sample() { + CONTEXT context{}; + context.ContextFlags = CONTEXT_FULL; + std::array frames; + int count = 0; + LARGE_INTEGER suspended{}; + LARGE_INTEGER resumed{}; + QueryPerformanceCounter(&suspended); + if (SuspendThread(target) == static_cast(-1)) return; + if (GetThreadContext(target, &context) != 0) count = walk(context, frames); ResumeThread(target); + QueryPerformanceCounter(&resumed); + if (pendingModule != 0) indexModule(); std::lock_guard lock(mutex); ++samples; - for (std::size_t i = 0; i < frames.size(); ++i) { - auto& count = counts[frames[i]]; - if (i == 0) ++count.first; - ++count.second; + suspendedTicks += resumed.QuadPart - suspended.QuadPart; + longestSuspendedTicks = std::max(longestSuspendedTicks, resumed.QuadPart - suspended.QuadPart); + for (int i = 0; i < count; ++i) { + auto& entry = counts[frames[i]]; + if (i == 0) ++entry.first; + ++entry.second; } + if (keepStacks && count != 0) stacks.Add({frames.data(), static_cast(count)}, context.Rip != 0); + } + + static std::string frameName(std::uint64_t address, std::map& loaded) { + HMODULE module = nullptr; + char name[MAX_PATH] = "?"; + if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) GetModuleFileNameA(module, name, sizeof(name)); + const char* base = std::strrchr(name, '\\'); + if (module != nullptr) loaded.emplace(reinterpret_cast(module), base ? base + 1 : name); + char frame[MAX_PATH + 32]; + std::snprintf(frame, sizeof(frame), "%s+0x%llx", base ? base + 1 : name, static_cast(address - reinterpret_cast(module))); + return frame; } void write() { @@ -81,6 +174,34 @@ struct Sampler { std::fprintf(file, "%s 0x%llx %llu %llu\n", base ? base + 1 : name, static_cast(address - reinterpret_cast(module)), static_cast(count.first), static_cast(count.second)); } std::fclose(file); + if (keepStacks) writeStacks(); + } + + void writeStacks() { + const auto dot = path.rfind('.'); + const auto slash = path.find_last_of("\\/"); + const auto stem = dot != std::string::npos && (slash == std::string::npos || dot > slash) ? path.substr(0, dot) : path; + const auto collapsed = stem + ".collapsed.txt"; + const auto temporary = collapsed + ".tmp"; + std::map names; + std::map loaded; + for (const auto& [address, count] : counts) names.emplace(address, frameName(address, loaded)); + std::FILE* file = std::fopen(temporary.c_str(), "w"); + if (file == nullptr) return; + LARGE_INTEGER frequency{}; + QueryPerformanceFrequency(&frequency); + const double tickUs = 1e6 / static_cast(frequency.QuadPart); + std::fprintf(file, "# samples=%llu depth=%d truncated=%llu suspended_us_mean=%.1f suspended_us_max=%.1f\n", static_cast(samples), depth, static_cast(stacks.Truncated()), + samples != 0 ? static_cast(suspendedTicks) * tickUs / static_cast(samples) : 0.0, static_cast(longestSuspendedTicks) * tickUs); + for (const auto& [base, name] : loaded) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + std::fprintf(file, "# module %s 0x%lx 0x%lx\n", name.c_str(), static_cast(nt->OptionalHeader.SizeOfImage), static_cast(nt->FileHeader.TimeDateStamp)); + } + const auto text = stacks.Format([&](std::uint64_t address) { return names.at(address); }); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + if (std::fclose(file) != 0 || !written) return; + MoveFileExA(temporary.c_str(), collapsed.c_str(), MOVEFILE_REPLACE_EXISTING); } }; @@ -172,6 +293,14 @@ void StartWorkerSampler() { if (path == nullptr) return; auto sampler = std::make_unique(); sampler->path = path; + sampler->depth = Sampler::WorkerDepth; + sampler->keepStacks = true; + const auto* block = reinterpret_cast(NtCurrentTeb()); + MEMORY_BASIC_INFORMATION stack{}; + if (VirtualQuery(block->StackLimit, &stack, sizeof(stack)) != 0) { + sampler->stackLow = reinterpret_cast(stack.AllocationBase); + sampler->stackHigh = reinterpret_cast(block->StackBase); + } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp index d5a82a6ed4..c586948030 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp @@ -18,11 +18,12 @@ enum class ColorTileMode : std::uint32_t { Standard64KB = 9, S4KBX = 0x15, D4KBX = 0x16, + D64KBX = 0x1a, RenderTarget = 0x1b }; constexpr bool ColorTileModeIsXor(ColorTileMode mode) { - return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::RenderTarget; + return mode == ColorTileMode::S4KBX || mode == ColorTileMode::D4KBX || mode == ColorTileMode::D64KBX || mode == ColorTileMode::RenderTarget; } constexpr std::uint32_t ColorTileModeBlockBytes(ColorTileMode mode) { @@ -30,7 +31,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 : 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 : 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 6ca5f6c43e..68ba7ea944 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -161,9 +161,11 @@ struct Context { bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; + bool shaderCullDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; + bool viewportIndexLayer = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 4b42880ecd..1c2fac1281 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -6,11 +6,13 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { class Texture; +class StorageTexture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); @@ -20,12 +22,23 @@ bool DepthSurfaceAt(std::uint64_t address); bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); void NoteHtileDepthClear(std::uint64_t htileAddress); +struct MemoryValueFill { + std::uint64_t address; + std::size_t bytes; + std::uint64_t valueAddress; +}; +std::optional MatchMemoryValueFill(std::span code, std::span userData, const std::array& numThreads, const std::array& groups); +void NoteMemoryValueFill(const MemoryValueFill& fill); bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture); void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile); std::uint64_t HtileMetadataBytes(VkExtent2D extent); bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes); std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); +VkFormat HostDepthFormat(const Context& context, VkFormat format); +float HostDepthBiasConstant(const Context& context, VkFormat format, float constant); +std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip); +bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index a0e1dc4ae9..2a74aebf18 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -33,6 +33,7 @@ struct HostImport { std::uint64_t serial = 0; bool unwatched = false; bool dmaBuf = false; + bool joined = false; std::shared_ptr chunk; }; @@ -52,13 +53,6 @@ ImportProbe ProbeImportWriteProtection(const Context& context); ImportProbe ProbeDmaBufImportWriteProtection(const Context& context); ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); -// Whether a host import that leaves its range write-watched can stamp the range's pages when it is -// made (the device's probe saw pages written at the import): its import window then stamps them as -// a possible CPU store, which a storage image resolves only by comparing with its guest-byte copy. -// Decides the device's import watch first; false without host imports or a write watch, true when -// the imports were not probed. The overload without a device reads the last decision (true before one). -bool WatchedImportsStampPages(const Context& context); -bool WatchedImportsStampPages(); // The host import of the registered allocation containing [address, address + bytes), made on demand // (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in @@ -238,6 +232,7 @@ public: struct AddressSpace; void WriteBack(); bool WritesOverlap(std::uint64_t address, std::size_t bytes) const; + bool CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const; // Guest ranges the shader may write through descriptors, as [begin, end). const std::vector>& Writes() const { return writes; } // Whether any written range lives in a copied buffer, so a write-back must run once the GPU is done. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 5d9d14728a..50674497b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include #include namespace AgcDriver::Graphics { @@ -30,6 +31,23 @@ private: std::string refusal; }; +class MultisampledColorFill { +public: + explicit MultisampledColorFill(const ColorTarget& target); + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + std::optional TakeFill(); + void Discard(); + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + std::optional filled; + std::string refusal; +}; + +std::uint64_t MultisampledColorBytes(const ColorTarget& target); +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); +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); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 6e6ca7fdf2..56878782df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -15,7 +15,7 @@ void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipel // the batch completes, so a recorded draw keeps the object (Recorder::Keep) like its pipeline. class Framebuffer { public: - Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent); + Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent, std::uint32_t layers = 1); ~Framebuffer(); Framebuffer(const Framebuffer&) = delete; Framebuffer& operator=(const Framebuffer&) = delete; @@ -47,7 +47,7 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent = {}); + std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers = 1, std::span persistent = {}); // Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const; // The same inside a render pass another pipeline of the same attachments began (compatible by @@ -68,6 +68,7 @@ private: std::vector> owners; std::vector owned; VkExtent2D extent; + std::uint32_t layers = 1; std::shared_ptr framebuffer; }; void release() noexcept; @@ -123,7 +124,7 @@ bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index 1d340829c4..b2ca74e468 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -21,6 +21,8 @@ struct PipelineLibraryKeys { std::vector layout; std::vector preRasterizationLayout; std::vector fragmentLayout; + std::uint32_t preRasterizationSets = 0; + std::uint32_t fragmentSets = 0; }; std::span PipelineLibraryDynamicStates(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 7b29f6df0c..51d1b86941 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -38,6 +38,7 @@ TextureCacheUse TextureCacheUsage(); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words); bool SampledTexturesShareEntry(std::span first, std::span second); +void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. @@ -166,6 +167,7 @@ public: bool UsesAddressTable() const { return usesBda; } std::size_t CopiedBytes() const { return guestMemory.CopiedBytes(); } bool WritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.WritesOverlap(address, bytes); } + bool CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.CopiedWritesOverlap(address, bytes); } // Whether a region the recorded work reads in place through a host import overlaps the range. bool ReadsOverlap(std::uint64_t address, std::size_t bytes) const; // The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp index a7e8bd9eca..76e24b0e23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Shaders.hpp @@ -73,7 +73,7 @@ inline VkShaderStageFlags PushConstantStages(std::span sha VkShaderStageFlags result = 0; for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh) result |= VulkanStage(shader.stage); + if (!shader.program->pushConstants.empty() || shader.stage == ShaderRecompiler::ShaderStage::Mesh || shader.stage == ShaderRecompiler::ShaderStage::TessellationControl) result |= VulkanStage(shader.stage); } return result; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 36a8348cc3..4ac57ebdca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include #include @@ -28,6 +29,8 @@ struct ShaderStages { std::uint32_t fragmentWaveSize; std::optional mesh; std::optional tessellation; + float minTessellationLevel = 1.0f; + float maxTessellationLevel = 64.0f; }; struct ColorTarget { @@ -58,6 +61,7 @@ struct ColorTarget { std::uint64_t arrayAddress = 0; std::uint32_t arrayLastSlice = 0; std::uint32_t arraySlice = 0; + std::uint32_t layers = 1; std::uint32_t exportIndex = 0; bool uintExport = false; std::uint32_t pipeBankXor = 0; @@ -102,6 +106,8 @@ struct State { bool rectList = false; bool dualSourceBlend = false; VkExtent2D renderExtent; + std::uint32_t renderLayers = 1; + std::uint32_t layerExports = 0; VkPrimitiveTopology topology; bool primitiveRestart = false; VkViewport viewport; @@ -118,16 +124,23 @@ struct State { std::uint32_t samples = 1; std::array sampleLocations{}; bool customSampleLocations = false; + std::uint32_t paClVsOutCntl = 0; }; +// PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two +// distance vectors); a vertex program that exports pos1..pos3 depends on them. +inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u); + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); +GuestTextureResource SurfaceForTarget(const ColorTarget& color); struct ColorMetadataPass { enum class Mode { EliminateFastClear, DccDecompress, Resolve }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index a01cd7efc7..a8eb2efeb9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -33,8 +33,6 @@ struct HostImport; VkFormat StorageFormatForGuest(const Context& context, std::uint32_t guestFormat); // Whether StorageFormatForGuest would succeed, without throwing (unknown guest formats included). bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); -// Whether a storage image of the guest format takes DCC clear `keys` as a GPU clear (see -// StorageTexture::upload); false for integer formats and non-clear keys. bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); @@ -63,12 +61,12 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); struct DepthCopyTag {}; 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); - bool IsDepthCopy() const { return depthCopy; } + bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; } bool RefreshDepthCopy(); struct DepthCopyCounts { std::uint64_t textures; @@ -101,6 +99,10 @@ public: // the source image's own (snapshots, views over other metadata); one per cache entry, so every // object binding the texture shares it. Under GuestMemory::GpuMutex only. DccKeyProof& KeyProof() const { return keyProof; } + enum class PerUseCopy : std::uint8_t { None, RefreshedOnReuse, RemadePerUse }; + PerUseCopy CopyKind() const { return perUseCopy; } + bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } + void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } private: void release() noexcept; @@ -127,7 +129,7 @@ private: std::shared_ptr source; std::shared_ptr storageSource; std::unique_ptr upload; - bool depthCopy = false; + PerUseCopy perUseCopy = PerUseCopy::None; bool copiedOnce = false; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; @@ -160,7 +162,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layers = 1, std::uint32_t layer = 0); VkImageView AttachmentProxyView(); void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; @@ -314,7 +316,7 @@ public: // GuestMemory::GpuMutex only, as Refresh is; never from a build's stage A. DccKeys UploadedKeys() const { return uploadedKeys; } DccKeys FilledKeys() const { return filledKeys; } - bool GuestSnapshotValid() const { return originalValid; } + bool GuestSnapshotValid() const { return originalHolds(descriptor.baseAddress, static_cast(guestBytes)); } DccKeyProof& KeyProof() const { return keyProof; } DccRangeProof& TargetKeyProof() const { return targetKeyProof; } DccKeys ProvedKeys() const; @@ -326,6 +328,7 @@ public: static std::uint64_t SinglePassMoves(); static std::uint64_t RefreshesProved(); std::uint64_t GuestBytes() const; + std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } private: @@ -336,6 +339,7 @@ private: // upload everything). void upload(const std::vector* layers = nullptr); void captureGuestBytes(const std::vector* layers); + bool originalHolds(std::uint64_t address, std::size_t bytes) const; bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize = false) const; // Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in // each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block @@ -533,7 +537,7 @@ private: std::map elementLayerViews; std::int8_t singlePassState = -1; bool attachable = false; - std::map, VkImageView> attachmentViews; + std::map, VkImageView> attachmentViews; VkImage proxyImage = VK_NULL_HANDLE; VkDeviceMemory proxyMemory = VK_NULL_HANDLE; VkImageView proxyView = VK_NULL_HANDLE; @@ -542,7 +546,7 @@ private: bool dirty = false; std::uint64_t version = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. - bool originalValid = true; + std::vector layerOriginal; // 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/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index c5de807ad4..8da4772769 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -180,6 +180,13 @@ inline std::size_t VertexBufferReadSize(const ShaderRecompiler::VertexAttribute& return static_cast(required); } +inline std::size_t VertexBufferReadOffset(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + const auto index = attribute.fetchIndex == 0 ? firstVertex : firstInstance; + return static_cast(static_cast(stride) * index); +} + struct VertexFetch { std::uint64_t begin; std::uint64_t end; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp index 0b2dde1f16..935a0e81af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/CmaskClear.comp @@ -29,10 +29,10 @@ void main() { uint nibble = ((y / 512u) * push.blocksPerRow + x / 1024u) * 8192u + offset; uint byteAddress = push.base + nibble / 2u; uint code = (cmask.words[byteAddress / 4u] >> ((byteAddress % 4u) * 8u + (nibble % 2u) * 4u)) & 0xfu; - if (code == 0xfu) return; - if (code == 0u && push.write != 0u) { + if (code != 0u) return; + if (push.write != 0u) { imageStore(target, ivec2(pixel), uvec4(push.clear0, push.clear1, 0u, 0u)); return; } - if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u << code); + if ((pixel.x & 7u) == 0u && (pixel.y & 7u) == 0u) atomicOr(errors.codes, 1u); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index 8323051957..699325b2e5 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 == 0x1b, "AGC graphics: unsupported color tile mode"); + require(mode == 0 || mode == 5 || mode == 9 || mode == 0x15 || mode == 0x16 || 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[3][5]; - static SwizzleTables tables[3][5]; - const auto modeIndex = mode == ColorTileMode::S4KBX ? 0u : mode == ColorTileMode::D4KBX ? 1u : 2u; + 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; const auto index = static_cast(std::countr_zero(bytesPerElement)); std::call_once(once[modeIndex][index], [&] { const auto* equation = FindTextureSwizzleEquation(static_cast(mode), bytesPerElement); @@ -118,6 +118,7 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, require(width != 0 && height != 0 && width <= 16384 && height <= 16384, "AGC graphics: invalid color surface extent"); require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); require(pipeBankXor == 0 || (ColorTileModeIsXor(mode) && pipeBankXor < ColorTileModeBlockBytes(mode) && pipeBankXor % 256u == 0), "AGC graphics: a color pipe/bank XOR applies only to whole 256-byte units of XOR swizzle blocks"); + require(mode != ColorTileMode::D64KBX || (pipeBankXor & ~0xcf00u) == 0, "AGC graphics: unsupported display color pipe/bank XOR column bits"); std::uint32_t paddedHeight = height; switch (mode) { case ColorTileMode::Linear: { @@ -127,9 +128,9 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, } case ColorTileMode::S4KBX: case ColorTileMode::D4KBX: + case ColorTileMode::D64KBX: case ColorTileMode::RenderTarget: { - // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. - const auto log2Elements = (mode == ColorTileMode::RenderTarget ? 16u : 12u) - static_cast(std::countr_zero(bytesPerElement)); + const auto log2Elements = static_cast(std::countr_zero(ColorTileModeBlockBytes(mode))) - static_cast(std::countr_zero(bytesPerElement)); blockWidth = 1u << ((log2Elements + 1u) / 2u); blockHeight = 1u << (log2Elements / 2u); pitch = (width + blockWidth - 1u) / blockWidth * blockWidth; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 5b9a4b6b7d..e1b0ba0099 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -57,6 +57,18 @@ DccKeys ByteKeys(std::uint8_t key) { } } +bool SingleKeysReadable(const GuestTextureResource& resource) { + return KeyBytes % BytesPerElement(resource.format) == 0; +} + +void ExpandSingleBlocks(std::span bytes, std::size_t elementBytes) { + for (std::size_t block = 0; block < bytes.size(); block += KeyBytes) { + const auto blockBytes = std::min(KeyBytes, bytes.size() - block); + auto* first = bytes.data() + block; + for (auto filled = std::min(elementBytes, blockBytes); filled < blockBytes; filled *= 2) std::memcpy(first + filled, first, std::min(filled, blockBytes - filled)); + } +} + // APS5_PROFILE_DRAW: key scans, their bytes and time, printed as [dcc] every 10 s, with the hits of // the GPU-store memo below and the "uncompressed" key stores by where they were made. Flush syncs // stay 0 (no key read syncs the recorder); the field is kept so the line's shape does not change. @@ -344,6 +356,7 @@ bool LayoutFor(VkFormat format, Layout& layout) { case VK_FORMAT_R16G16_UINT: layout = {2, {16, 16}, Kind::Uint}; return true; case VK_FORMAT_R16G16_SINT: layout = {2, {16, 16}, Kind::Sint}; return true; case VK_FORMAT_R16G16_SFLOAT: layout = {2, {16, 16}, Kind::Float}; return true; + case VK_FORMAT_B10G11R11_UFLOAT_PACK32: layout = {3, {11, 11, 10}, Kind::Float}; return true; case VK_FORMAT_A2B10G10R10_UNORM_PACK32: case VK_FORMAT_A2R10G10B10_UNORM_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Unorm}; return true; case VK_FORMAT_A2B10G10R10_UINT_PACK32: layout = {4, {10, 10, 10, 2}, Kind::Uint}; return true; case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: layout = {4, {8, 8, 8, 8}, Kind::Unorm}; return true; @@ -370,7 +383,14 @@ std::uint64_t One(Kind kind, std::uint32_t bits) { switch (kind) { case Kind::Unorm: case Kind::Uint: return bits >= 64 ? ~0ull : (1ull << bits) - 1u; case Kind::Snorm: case Kind::Sint: return (1ull << (bits - 1u)) - 1u; - case Kind::Float: return bits == 16 ? 0x3c00u : 0x3f800000u; + case Kind::Float: + switch (bits) { + case 10: return 0x1e0u; + case 11: return 0x3c0u; + case 16: return 0x3c00u; + case 32: return 0x3f800000u; + default: throw std::runtime_error("AGC graphics: DCC clear of a " + std::to_string(bits) + "-bit float channel is not implemented"); + } } return 0; } @@ -448,8 +468,10 @@ DccKeys readDccKeys(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; - const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized); - if (keys == DccKeys::Uncompressed) return keys; + DccKeys keys; + if (const auto known = WaitForKeyWriters(resource, guestBytes)) keys = *known; + else keys = readDccKeys(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]{}; if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) { @@ -656,13 +678,15 @@ DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceByt void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span bytes) { // Named for the [hooksync] attribution: the read goes through the flush hook. const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureRead); - if (keys == DccKeys::Uncompressed) GuestMemory::ReadCommitted(resource.baseAddress, bytes); + if (keys == DccKeys::Uncompressed || keys == DccKeys::ClearSingle) GuestMemory::ReadCommitted(resource.baseAddress, bytes); else FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, bytes); + if (keys == DccKeys::ClearSingle) ExpandSingleBlocks(bytes, BytesPerElement(resource.format)); } namespace { DccKeys EncodedKeys(const GuestTextureResource& resource, DccKeys keys) { + if (keys == DccKeys::ClearSingle && SingleKeysReadable(resource)) return keys; std::byte probe[16]{}; if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min(sizeof(probe), BytesPerElement(resource.format))))) return DccKeys::Uncompressed; return keys; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 406470b233..9337c6ef16 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1,8 +1,12 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include @@ -10,13 +14,17 @@ #include #include #include +#include #include #include +#include #include namespace AgcDriver::Graphics { namespace { +std::uint64_t depthWrites = 0; + Recorder* RecorderFor(const Context& context) { auto* recorder = Recorder::Active(); return recorder != nullptr && recorder->Device() == context.device ? recorder : nullptr; @@ -24,25 +32,25 @@ Recorder* RecorderFor(const Context& context) { class DepthSurface { public: - DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target) { + DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)) { this->context.bufferPool.reset(); VkFormatProperties properties{}; - context.formatProperties(context.physical, target.format, &properties); - Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); - Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be sampled on this device"); + context.formatProperties(context.physical, hostFormat, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be an attachment on this device"); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(hostFormat) + " cannot be sampled on this device"); Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - if (SampleLocationsCompatibleDepth(context, target.format, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; + if (SampleLocationsCompatibleDepth(context, hostFormat, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; - info.format = target.format; + info.format = hostFormat; info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; info.arrayLayers = 1; info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -56,7 +64,7 @@ public: VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = target.format; + viewInfo.format = hostFormat; viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); auto* recorder = RecorderFor(context); @@ -92,6 +100,7 @@ public: const auto aspects = pendingClear & (VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)); pendingClear = 0; if (aspects == 0) return; + written = ++depthWrites; auto* recorder = RecorderFor(context); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -120,14 +129,16 @@ public: const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + const bool oneSlice = resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); + if ((format != expected && !depthBits) || !oneSlice || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { char text[448]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); throw std::runtime_error(text); } - auto texture = std::make_shared(context, image, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components); + const auto viewType = resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; + auto texture = std::make_shared(context, image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType); textures.emplace(key, texture); return texture; } @@ -136,19 +147,106 @@ public: return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; }); } + std::uint64_t written = ++depthWrites; + VkImageAspectFlags pendingClear = 0; 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; + 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); + } + Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented"); + if (!stencil && hostFormat != target.format) { + char text[224]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth plane of depth surface 0x%llx (vk format %d) stored as vk format %d is not implemented", static_cast(target.address), static_cast(target.format), static_cast(hostFormat)); + throw std::runtime_error(text); + } + if (mip != 0 || resource.baseLevel != 0) { + char text[224]; + 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); + } + 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) { + std::erase_if(plane.replaced, [](const auto& old) { return old.expired(); }); + if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); + plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + } + copyPlane(plane, stencil, true); + return plane.storage; + } + + bool StoreBack(const StorageTexture& candidate) { + for (const bool stencil : {false, true}) { + auto& plane = stencil ? stencilPlane : depthPlane; + if (plane.storage.get() == &candidate) { + copyPlane(plane, stencil, false); + written = ++depthWrites; + return true; + } + if (std::none_of(plane.replaced.begin(), plane.replaced.end(), [&](const auto& old) { return old.lock().get() == &candidate; })) continue; + char text[288]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image writes to the %s plane of depth surface 0x%llx through a view that another view of the plane (guest format %u, tile mode %d) replaced are not implemented", stencil ? "stencil" : "depth", static_cast(target.address), plane.storage->Descriptor().format, static_cast(plane.storage->Descriptor().tileMode)); + throw std::runtime_error(text); + } + return false; + } + const Context context; const DepthTarget target; + const VkFormat hostFormat; VkImage image = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; private: + struct PlaneStorage { + std::shared_ptr storage; + std::vector> replaced; + std::unique_ptr staging; + }; std::map, std::shared_ptr> textures; + PlaneStorage depthPlane; + PlaneStorage stencilPlane; + + void copyPlane(PlaneStorage& plane, bool stencil, bool toStorage) { + auto* recorder = Recorder::Active(); + Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); + if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder->Commands(); + recorder->Keep(plane.storage); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + VkBufferImageCopy colorRegion = planeRegion; + colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + const auto sourceImage = toStorage ? image : plane.storage->Image(); + const auto destinationImage = toStorage ? plane.storage->Image() : image; + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + recorder->EndGpuTiming(timing, bytes); + } void release() noexcept { + depthPlane = {}; + stencilPlane = {}; textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); @@ -159,6 +257,188 @@ private: } }; +struct DepthPlaneSlice { + VkImage image = VK_NULL_HANDLE; + std::uint64_t written = 0; +}; + +class DepthPlaneCopy { +public: + DepthPlaneCopy(const Context& context, VkExtent2D extent, VkFormat format, std::uint32_t layers) : context(context), extent(extent), format(format), layers(layers), guestLayers(layers), surfaceLayers(layers) { + this->context.bufferPool.reset(); + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = layers % 6u == 0 && extent.width == extent.height ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + info.imageType = VK_IMAGE_TYPE_2D; + 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 = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + try { + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth plane copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth plane copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); + staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (...) { + release(); + throw; + } + } + ~DepthPlaneCopy() { release(); } + DepthPlaneCopy(const DepthPlaneCopy&) = delete; + DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; + + std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { + Require(slices.size() == layers, "depth plane copy slices do not match its layers"); + const auto geometry = DescribeSurface(resource); + Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); + std::vector> uploads; + std::vector uploadLayers; + std::vector copiedLayers; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + auto& held = guestLayers[layer]; + auto& surface = surfaceLayers[layer]; + if (slices[layer].image != VK_NULL_HANDLE) { + held = {}; + if (slices[layer].written != 0 && surface.image == slices[layer].image && surface.written == slices[layer].written) continue; + surface = slices[layer]; + copiedLayers.push_back(layer); + continue; + } + surface = {}; + const GuestLayer current{resource.baseAddress + geometry.GuestLayerOffset(layer), geometry.layerBytes, resource.tileMode, geometry.thick, GuestMemory::CollectWrites(resource.baseAddress + geometry.GuestLayerOffset(layer), static_cast(geometry.layerBytes))}; + if (held.generation != 0 && held.address == current.address && held.bytes == current.bytes && held.tileMode == current.tileMode && held.thick == current.thick && !Recorder::SnapshotWriteOverlaps(current.address, static_cast(current.bytes)) && GuestMemory::UnchangedSince(current.address, static_cast(current.bytes), held.generation)) { + held.generation = current.generation; + continue; + } + auto upload = std::make_shared(context, static_cast(geometry.layerBytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + GuestMemory::ReadCommitted(current.address, upload->Bytes().first(static_cast(geometry.layerBytes))); + uploads.push_back(std::move(upload)); + uploadLayers.push_back(layer); + held = current; + } + if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; + auto& texture = textures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, image, format, aspect(), components, viewType); + texture->MarkRefreshedPerUse(); + } + return texture; + } + + const Context context; + const VkExtent2D extent; + const VkFormat format; + const std::uint32_t layers; + +private: + struct GuestLayer { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + TextureTileMode tileMode = TextureTileMode::kLinear; + bool thick = false; + std::uint64_t generation = 0; + }; + + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr staging; + std::map, std::shared_ptr> textures; + std::vector guestLayers; + std::vector surfaceLayers; + bool filled = false; + + void record(std::span slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span copiedLayers, std::span> uploads, std::span uploadLayers) { + if (!uploads.empty()) { + Require(context.detiler != nullptr, "depth plane copy requires a texture detiler"); + context.detiler->BeginBatch(); + } + 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(); + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + std::vector regions; + const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { + VkBufferImageCopy copy{}; + copy.bufferOffset = sliceBytes() * layer; + copy.imageSubresource = {aspectMask, 0, imageLayer, 1}; + copy.imageExtent = {extent.width, extent.height, 1}; + return copy; + }; + for (const auto layer : copiedLayers) { + const auto copy = region(layer, VK_IMAGE_ASPECT_DEPTH_BIT, 0); + context.Function("vkCmdCopyImageToBuffer")(commands, slices[layer].image, VK_IMAGE_LAYOUT_GENERAL, staging->Handle(), 1, ©); + regions.push_back(region(layer, aspect(), layer)); + } + if (!uploads.empty()) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::size_t upload = 0; upload < uploads.size(); ++upload) { + const auto layer = uploadLayers[upload]; + context.detiler->Dispatch(commands, resource.tileMode, format == VK_FORMAT_D32_SFLOAT ? 4u : 2u, uploads[upload]->Handle(), 0, staging->Handle(), geometry.LinearLayerOffset(layer), geometry.mips.front(), false, layer, geometry.thick); + regions.push_back(region(layer, aspect(), layer)); + } + if (recorder != nullptr) { + for (const auto& upload : uploads) recorder->Keep(upload); + } + } + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = filled ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = image; + toTransfer.subresourceRange = {aspect(), 0, 1, 0, layers}; + const VkBufferMemoryBarrier staged{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, staging->Handle(), 0, VK_WHOLE_SIZE}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &staged, 1, &toTransfer); + context.Function("vkCmdCopyBufferToImage")(commands, staging->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + VkImageMemoryBarrier toGeneral = toTransfer; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_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_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + filled = true; + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + } + + VkDeviceSize sliceBytes() const { + return static_cast(extent.width) * extent.height * (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); + } + + VkImageAspectFlags aspect() const { + return format == VK_FORMAT_R16_UINT ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; + } + + void release() noexcept { + textures.clear(); + staging.reset(); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +std::map, std::unique_ptr>& planeCopies() { + static auto* copies = new std::map, std::unique_ptr>(); + return *copies; +} + 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; } @@ -178,6 +458,8 @@ std::set& clearedHtiles() { return *cleared; } +const DepthSurface* drawnDepth = nullptr; + } std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { @@ -198,6 +480,32 @@ bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std return (properties.sampleCounts & samples) != 0; } +VkFormat HostDepthFormat(const Context& context, VkFormat format) { + if (format != VK_FORMAT_D16_UNORM_S8_UINT) return format; + static std::mutex mutex; + static std::map chosen; + std::lock_guard lock(mutex); + if (const auto found = chosen.find(context.physical); found != chosen.end()) return found->second; + constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + auto host = format; + for (const auto candidate : {VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, candidate, &properties); + if ((properties.optimalTilingFeatures & required) != required) continue; + host = candidate; + break; + } + chosen.emplace(context.physical, host); + return host; +} + +float HostDepthBiasConstant(const Context& context, VkFormat format, float constant) { + const auto host = HostDepthFormat(context, format); + if (constant == 0.0f || host == format) return constant; + Require(host == VK_FORMAT_D24_UNORM_S8_UINT, "a constant depth bias on a 16-bit depth target with stencil that the device stores as 32-bit float depth is not implemented"); + return constant * 256.0f; +} + VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; @@ -207,9 +515,14 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) surface->clearStencil = target.clearStencil; if (cleared) surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; surface->ApplyFastClear(); + surface->written = ++depthWrites; + drawnDepth = surface.get(); return surface->view; } 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; } @@ -221,6 +534,31 @@ std::uint64_t HtileDepthClearAddress(std::span code, std::s return userData[2] | (static_cast(userData[3]) << 32u); } +std::optional MatchMemoryValueFill(std::span code, std::span userData, const std::array& numThreads, const std::array& groups) { + static constexpr std::array kernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + if (userData.size() < 8 || numThreads != std::array{64u, 1u, 1u} || groups[1] != 1u || groups[2] != 1u) return std::nullopt; + if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + const auto stride = (userData[1] >> 16u) & 0x3fffu; + const bool swizzled = ((userData[1] >> 31u) & 1u) != 0; + const bool addTid = ((userData[3] >> 23u) & 1u) != 0; + const auto type = userData[3] >> 30u; + const auto format = (userData[3] >> 12u) & 0x7fu; + const auto valueStride = (userData[5] >> 16u) & 0x3fffu; + const auto valueBytes = valueStride == 0 ? static_cast(userData[6]) : static_cast(userData[6]) * valueStride; + if (type != 0 || stride != 4 || swizzled || addTid || format < 20u || format > 22u || valueBytes < 4u) return std::nullopt; + const auto dwords = std::min(static_cast(groups[0]) * 64u, userData[2]); + const auto address = userData[0] | (static_cast(userData[1] & 0xffffu) << 32u); + const auto valueAddress = userData[4] | (static_cast(userData[5] & 0xffffu) << 32u); + if (dwords == 0 || valueAddress == 0 || (valueAddress & 3u) != 0) return std::nullopt; + return MemoryValueFill{address, static_cast(dwords * 4u), valueAddress}; +} + +void NoteMemoryValueFill(const MemoryValueFill& fill) { + std::uint32_t value = 0; + GuestMemory::Read(fill.valueAddress, std::as_writable_bytes(std::span(&value, 1)), 4); + NoteDepthMetadataFill(fill.address, fill.bytes, value); +} + void NoteHtileDepthClear(std::uint64_t htileAddress) { std::lock_guard lock(surfacesMutex()); bool existing = false; @@ -234,28 +572,61 @@ void NoteHtileDepthClear(std::uint64_t htileAddress) { void ClearDepthSurfaces(VkDevice device) { std::lock_guard lock(surfacesMutex()); + drawnDepth = nullptr; 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::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + const auto held = [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }; + { + std::lock_guard lock(surfacesMutex()); + if (std::none_of(surfaces().begin(), surfaces().end(), held)) return nullptr; + } + std::lock_guard gpu(GuestMemory::GpuMutex()); 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 nullptr; - const auto& target = (*found)->target; - if (resource.width != target.extent.width || resource.height != target.extent.height) return nullptr; - const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; - if (!stencil) { - const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; - const bool depthBits = words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); - if (ResolveTextureFormat(resource.format) != (d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT) && !depthBits) return nullptr; - } else if (BytesPerElement(resource.format) != 1u) { - 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; + const auto format = ResolveTextureFormat(resource.format); + const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); + const bool integer = d16 && format == VK_FORMAT_R16_UINT && !depthBits; + if (!stencil && format != (d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT) && !depthBits && !integer) return nullptr; + if (stencil && BytesPerElement(resource.format) != 1u) return nullptr; (*found)->ApplyFastClear(); - return (*found)->Sampled(words, resource, components); + 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"); + 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)) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast(base.address), base.extent.width, base.extent.height, static_cast(base.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); + throw std::runtime_error(text); + } + std::vector 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}); + } + 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}]; + if (copy == nullptr) { + copy = std::make_unique(context, base.extent, imageFormat, layers); + } + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { @@ -311,6 +682,23 @@ bool DepthSurfaceAt(const Context& context, std::span words return ResolveTextureFormat(resource.format) == expected || depthBits; } +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); +} + +bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == context.device && surface->StoreBack(storage)) return true; + } + return false; +} + bool DepthSurfaceAt(std::uint64_t address) { std::lock_guard lock(surfacesMutex()); return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3cb6a005c3..68f487b489 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -61,38 +61,6 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: namespace { -std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { - if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest; - throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast(format))); -} - -// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). -GuestTextureResource SurfaceForTarget(const ColorTarget& color) { - Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); - const bool chain = color.mipCount > 1; - GuestTextureResource surface{}; - surface.baseAddress = chain ? color.surfaceAddress : color.address; - surface.pipeBankXor = color.pipeBankXor; - surface.width = chain ? color.surfaceExtent.width : color.extent.width; - surface.height = chain ? color.surfaceExtent.height : color.extent.height; - surface.depthOrLastArray = color.depth - 1u; - surface.baseArray = 0; - surface.mipCount = color.mipCount; - surface.baseLevel = 0; - surface.lastLevel = color.mipCount - 1; - surface.tileMode = ColorTextureTileMode(color.tileMode); - surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D; - surface.format = GuestFormatFor(color.format, color.elementBytes); - surface.dstSelX = 4; - surface.dstSelY = 5; - surface.dstSelZ = 6; - surface.dstSelW = 7; - surface.dccAddress = color.dccAddress; - surface.dccAlphaOnMsb = color.dccAlphaOnMsb; - surface.dccPipeAligned = color.dccPipeAligned; - return surface; -} - GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array"); auto surface = SurfaceForTarget(color); @@ -221,20 +189,8 @@ void proveCmask(std::uint64_t address, std::size_t bytes, std::uint64_t generati else cmaskProofs()[address] = {bytes, generation}; } -std::string hexAddress(std::uint64_t address) { - char text[24]; - std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); - return text; -} - void requireCmaskCodes(std::uint32_t codes) { - if (codes == 0) return; Require((codes & 1u) == 0, "a CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys, was recorded"); - std::string listed; - for (std::uint32_t code = 1; code < 15; ++code) { - if ((codes >> code) & 1u) listed += (listed.empty() ? "" : ", ") + std::to_string(code); - } - Require(false, "CMASK codes " + listed + " were recorded, which are not a single-sample fast-clear code (0 cleared, 15 expanded)"); } CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::vector& cleared) { @@ -264,7 +220,6 @@ CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::v const auto nibble = layout.Nibble(tileX, tileY); const auto code = (std::to_integer(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu; if (code == 0u) cleared.push_back(tileY * layout.TilesX() + tileX); - else Require(code == 0xfu, "CMASK code " + std::to_string(code) + " of color target " + hexAddress(color.address) + " is not a single-sample fast-clear code (0 cleared, 15 expanded)"); } } if (cleared.empty()) return CmaskState::Expanded; @@ -686,6 +641,10 @@ bool ValidationKey(const Context& context, std::span shade add(context.subgroup.supportedStages); add(context.subgroup.supportedOperations); add(context.fragmentShaderBarycentric); + add(context.shaderClipDistance); + add(context.shaderCullDistance); + add(context.viewportIndexLayer); + add(state.paClVsOutCntl); add(state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; @@ -706,6 +665,7 @@ bool ValidationKey(const Context& context, std::span shade add(tessellation.domain); add(tessellation.partitioning); add(tessellation.outputTopology); + add(tessellation.patchesPerGroup); } } return keyed; @@ -737,7 +697,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); @@ -1176,7 +1136,8 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance); const auto& fields = attribute.resource.fields; const auto address = fields[0] | (static_cast(fields[1] & 0xffffu) << 32u); - Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address, "vertex buffer aliases the render target"); + const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0; + Require(!state.hasColorTarget || address + bytes <= state.color.address || state.color.address + state.color.bytes <= address + firstByte, "vertex buffer aliases the render target"); const auto stride = (fields[1] >> 16u) & 0x3fffu; fetchOf[i] = fetches.size(); fetches.push_back({address, address + bytes, stride, attribute.fetchIndex, DecodeVertexFormat(attribute).alignment}); @@ -1587,6 +1548,12 @@ void pushDrawConstants(const Pipeline& pipeline, VkCommandBuffer commands, const resources.PatchPushConstants(block); stages = PushConstantStages(shaders); } + if (state.stages.tessellation) { + for (const auto& shader : shaders) Require(shader.program->pushConstants.empty() || shader.pushConstantOffset >= PipelinePushSlotBytes || shader.pushConstantOffset + shader.program->pushConstants.size() <= ShaderRecompiler::TessellationLevelPushOffsetBytes, "a stage's push constants overlap the tessellation levels"); + const std::array levels{state.stages.minTessellationLevel, state.stages.maxTessellationLevel}; + static_assert(ShaderRecompiler::TessellationLevelPushOffsetBytes + ShaderRecompiler::TessellationLevelPushBytes == PipelinePushSlotBytes); + std::memcpy(block.data() + ShaderRecompiler::TessellationLevelPushOffsetBytes, levels.data(), sizeof(levels)); + } if (!state.stages.mesh) { pipeline.PushConstants(commands, stages, block); return; @@ -1965,7 +1932,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED; - targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice)); + targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.layers, color.arraySlice)); continue; } materializeCmaskClear(context, color, nullptr); @@ -2037,7 +2004,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& return IndirectDrawPath::PendingImage; } if (recorder != nullptr && recorder->PendingLabelIn(address, bytes)) return IndirectDrawPath::PendingLabelOrCopy; - const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; + const auto overlaps = [&](const auto& writer) { return writer->CopiedWritesOverlap(address, bytes); }; if (context.copiedWriters != nullptr && std::any_of(context.copiedWriters->begin(), context.copiedWriters->end(), overlaps)) return IndirectDrawPath::PendingLabelOrCopy; if (std::any_of(DrawCopiedWriters()->begin(), DrawCopiedWriters()->end(), overlaps)) return IndirectDrawPath::PendingLabelOrCopy; if ((import = HostImportFor(context, address, bytes)) == nullptr) return IndirectDrawPath::NotImported; @@ -2153,7 +2120,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& owners.push_back(binding.resident); persistent.push_back(binding.multisampled ? 1u : 0u); } - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent); + auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2529,7 +2496,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con auto pipeline = recipe.pipeline.lock(); if (pipeline == nullptr) return miss(DrawRecipeMiss::ObjectsGone); auto framebuffer = recipe.framebuffer.lock(); - if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent); + if (framebuffer == nullptr) framebuffer = pipeline->AcquireFramebuffer(recipe.targetViews, targets, state.renderExtent, state.renderLayers); timer.phase(PhasePipeline); const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; RecordedDraw record; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 880720c82c..dcc394cc75 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -159,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; @@ -168,10 +172,6 @@ struct HostImports { VkDevice watchDevice = VK_NULL_HANDLE; bool unwatchImports = false; bool unwatchDmaBufImports = false; - // Whether making an import of each kind was seen to stamp the imported pages (the probe's - // writtenAtImport): its import window then stamps a range the import leaves watched. - bool importsStampPages = true; - bool dmaBufImportsStampPages = true; }; HostImports& Imports() { @@ -212,12 +212,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)); @@ -480,20 +481,39 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, } #endif +#ifndef _WIN32 +struct BackingFile { + BackingFile() = default; + BackingFile(const BackingFile&) = delete; + BackingFile& operator=(const BackingFile&) = delete; + ~BackingFile() { + if (file >= 0) close(file); + } + int file = -1; + std::uint64_t offset = 0; +}; + +bool sharedBacking(const Context& context, const HostImport& entry, BackingFile& backing) { + return context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(entry.base), static_cast(entry.bytes), &backing.file, &backing.offset); +} + +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); +} +#endif + const char* createImport(const Context& context, HostImport& entry, VkResult& failure) { const char* step = createHostPointerImport(context, entry, failure); #ifndef _WIN32 - int file = -1; - std::uint64_t offset = 0; - if (step != nullptr && context.dmaBufImport && GuestArena::GuestArenaSharedBacking_nid_postfix(static_cast(entry.base), static_cast(entry.bytes), &file, &offset)) { - bool fits = false; - VkResult chunkFailure = VK_SUCCESS; - if (createChunkedDmaBufImport(context, entry, file, offset, fits, chunkFailure) == nullptr && fits) { - close(file); - return nullptr; - } - step = createDmaBufImport(context, entry, file, offset, failure); - } + BackingFile backing; + if (step != nullptr && sharedBacking(context, entry, backing)) step = createBackingImport(context, entry, backing, failure); #endif return step; } @@ -519,21 +539,14 @@ void decideImportWatch(const Context& context, HostImports& state) { state.watchDevice = context.device; state.unwatchImports = true; state.unwatchDmaBufImports = false; - state.importsStampPages = false; - state.dmaBufImportsStampPages = false; if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] Windows direct host imports use comparisons; separate shared aliases retain guest write tracking\n"); #else const auto request = importWatchRequest(); state.watchDevice = context.device; state.unwatchImports = false; state.unwatchDmaBufImports = false; - state.importsStampPages = false; - state.dmaBufImportsStampPages = false; if (context.hostImportAlignment == 0 || !GuestMemory::WriteWatched()) return; if (request == ImportWatchRequest::Watch) { - // Not probed: whether an import stamps its pages is unknown. - state.importsStampPages = true; - state.dmaBufImportsStampPages = true; std::fprintf(stderr, "[write-watch] host imports stay watched (APS5_WRITE_WATCH_IMPORTS=watch)\n"); return; } @@ -549,11 +562,9 @@ void decideImportWatch(const Context& context, HostImports& state) { std::fprintf(stderr, "[write-watch] host imports resolve write protection: unknown (probe failed at %s, %d); imported ranges are compared\n", probe.failure, static_cast(probe.result)); } else { state.unwatchImports = probe.writtenAfterSubmit != 0 || probe.writtenAfterFill != 0; - state.importsStampPages = probe.writtenAtImport != 0; std::fprintf(stderr, "[write-watch] host imports resolve write protection: %s (%u of %u scratch pages written after a GPU read, %u after a GPU write, %u after the import); imported ranges %s\n", state.unwatchImports ? "yes" : "no", probe.writtenAfterSubmit, probe.pages, probe.writtenAfterFill, probe.writtenAtImport, state.unwatchImports ? "are compared" : "stay watched"); } state.unwatchDmaBufImports = state.unwatchImports; - state.dmaBufImportsStampPages = state.importsStampPages; if (!context.dmaBufImport) return; const auto dmaBuf = ProbeDmaBufImportWriteProtection(context); if (dmaBuf.failure != nullptr) { @@ -562,7 +573,6 @@ void decideImportWatch(const Context& context, HostImports& state) { return; } state.unwatchDmaBufImports = dmaBuf.writtenAfterSubmit != 0 || dmaBuf.writtenAfterFill != 0 || dmaBuf.writtenByCpu == 0; - state.dmaBufImportsStampPages = dmaBuf.writtenAtImport != 0; std::fprintf(stderr, "[write-watch] dma-buf imports keep write protection: %s (%u of %u scratch pages written after a GPU read, %u after a GPU write, %u after the import, %u seen after a CPU store); imported ranges %s\n", state.unwatchDmaBufImports ? "no" : "yes", dmaBuf.writtenAfterSubmit, dmaBuf.pages, dmaBuf.writtenAfterFill, dmaBuf.writtenAtImport, dmaBuf.writtenByCpu, state.unwatchDmaBufImports ? "are compared" : "stay watched"); #endif } @@ -623,6 +633,27 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u return !run.ranges.empty() && end <= run.base + run.bytes; } +ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + const auto window = importWindow.load(std::memory_order_relaxed); + auto last = leasedIndexContaining(lease, begin); + if (window == 0 || last == lease.size()) return {}; + const auto first = last; + for (;;) { + if (lease[last]->bytes <= window) return {}; + if (lease[last]->address + lease[last]->bytes >= end) break; + 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; +} + +bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end) { + return !joinedRun(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; @@ -632,21 +663,55 @@ bool sameRun(const Entry& entry, const ImportRun& run) { return true; } +bool wouldRetireImport(const HostImports& state, const ImportRun& run) { + if (run.ranges.empty()) return false; + const auto next = state.imports.lower_bound(run.base); + if (next != state.imports.end()) { + if (next->first == run.base && sameRun(next->second, run)) return false; + if (next->first < run.base + run.bytes) return true; + } + if (next == state.imports.begin()) return false; + const auto& previous = std::prev(next)->second; + return previous.base + previous.bytes > run.base; +} + bool sameRange(const HostImport& entry, const GuestAllocations::Lease& lease) { - return sameRun(entry, importRun(lease, entry.base)); + return sameRun(entry, entry.joined ? joinedRun(lease, entry.base, entry.base + entry.bytes) : importRun(lease, entry.base)); } 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); 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; + } else if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + return current; } auto run = importRun(lease, address); if (run.ranges.empty()) return nullptr; + return importRunAt(context, state, std::move(run), false, address, lease); +} + +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); +} + +const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease) { const auto base = run.base; const auto bytes = run.bytes; + auto& failures = joined ? state.failedJoins : state.failed; + const auto alignment = context.hostImportAlignment; + if (joined && (alignment == 0 || base % alignment != 0 || bytes % alignment != 0)) { + failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); + return nullptr; + } 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); @@ -657,11 +722,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s 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); - 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 (const auto failed = failures.find(base); failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; - state.failed.erase(failed); + failures.erase(failed); } // 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 @@ -694,7 +758,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; @@ -706,7 +770,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) { @@ -719,33 +783,48 @@ 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); } } #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 decideImportWatch(context, state); VkResult result = VK_SUCCESS; const char* step = nullptr; - GuestMemory::ImportWatched(base, bytes, [&] { - step = createImport(context, entry, result); - return step == nullptr; - }); - entry.unwatched = step == nullptr && (entry.dmaBuf ? state.unwatchDmaBufImports : state.unwatchImports); #ifdef _WIN32 - if (entry.alias != nullptr) entry.unwatched = false; + const bool watchedHostPointer = entry.alias != nullptr || !state.unwatchImports; + const bool watchedDmaBuf = watchedHostPointer; +#else + const bool watchedHostPointer = !state.unwatchImports; + const bool watchedDmaBuf = !state.unwatchDmaBufImports; #endif - if (entry.unwatched) GuestMemory::Unwatch(base, bytes); + GuestMemory::ImportWatched(base, bytes, [&] { + step = watchedDmaBuf == watchedHostPointer ? createImport(context, entry, result) : createHostPointerImport(context, entry, result); + return step == nullptr; + }, watchedHostPointer); +#ifndef _WIN32 + if (BackingFile backing; step != nullptr && watchedDmaBuf != watchedHostPointer && sharedBacking(context, entry, backing)) { + GuestMemory::ImportWatched(base, bytes, [&] { + step = createBackingImport(context, entry, backing, result); + return step == nullptr; + }, watchedDmaBuf); + } +#endif + entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { #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; @@ -775,7 +854,8 @@ 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; return &state.imports.emplace(base, entry).first->second; } @@ -809,6 +889,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"); @@ -834,6 +915,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc 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) { @@ -1782,6 +1864,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; @@ -1958,27 +2041,6 @@ ImportWatch PrepareImportWatch(const Context& context) { return state.unwatchImports ? ImportWatch::Unwatch : ImportWatch::Watch; } -namespace { - -bool stampsWatchedPages(const HostImports& state) { - return (!state.unwatchImports && state.importsStampPages) || (!state.unwatchDmaBufImports && state.dmaBufImportsStampPages); -} - -} - -bool WatchedImportsStampPages(const Context& context) { - auto& state = Imports(); - std::lock_guard lock(state.mutex); - decideImportWatch(context, state); - return stampsWatchedPages(state); -} - -bool WatchedImportsStampPages() { - auto& state = Imports(); - std::lock_guard lock(state.mutex); - return state.watchDevice == VK_NULL_HANDLE || stampsWatchedPages(state); -} - void SetImportWatch(const Context& context, ImportWatch watch) { auto& state = Imports(); std::lock_guard lock(state.mutex); @@ -2002,7 +2064,7 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); refreshImports(context, state, lease); - if (!runCovers(lease, address, address + bytes)) return nullptr; + if (!runCovers(lease, address, address + bytes)) return findImport(state, address, address + bytes); const auto* entry = importAllocation(context, state, address, lease); return entry != nullptr && findImport(state, address, address + bytes) == entry ? entry : nullptr; } @@ -2026,6 +2088,7 @@ 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); } 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()); @@ -2247,6 +2310,20 @@ void GuestBufferMemory::AcquireRegistered() { publish = false; spaces.unpublished.fetch_add(1, std::memory_order_relaxed); } else { + std::size_t kept = 0; + for (std::size_t index = 0; index < regions.size(); ++index) { + auto& region = regions[index]; + if (kept != 0) { + auto& previous = regions[kept - 1]; + if (region.direct != nullptr && previous.direct == region.direct && previous.end == region.begin && previous.writable == region.writable && region.mirror == nullptr && previous.mirror == nullptr && !region.sparse && !previous.sparse && region.end <= region.direct->base + region.direct->bytes) { + previous.end = region.end; + continue; + } + } + if (kept != index) regions[kept] = std::move(region); + ++kept; + } + regions.erase(regions.begin() + static_cast(kept), regions.end()); for (const auto& region : regions) { if (region.direct != nullptr || region.mirror != nullptr) ranges.push_back(addressRange(region)); } @@ -2727,7 +2804,9 @@ bool GuestBufferMemory::stagingEligible(const Region& region, bool addressable) void GuestBufferMemory::UploadPrepare(bool addressable) { Require(!prepared && !uploaded, "guest memory was already uploaded"); prepared = true; - std::sort(regions.begin(), regions.end(), [](const Region& left, const Region& right) { return left.begin < right.begin; }); + std::vector order(regions.size()); + std::iota(order.begin(), order.end(), 0u); + std::sort(order.begin(), order.end(), [&](std::uint32_t left, std::uint32_t right) { return regions[left].begin < regions[right].begin; }); // Merged regions stay sparse when either part covered uncommitted pages. const auto mergeBacked = [](Region& into, const Region& from) { if (!into.sparse && !from.sparse) return; @@ -2745,7 +2824,9 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { if (!into.sparse) into.backed.clear(); }; std::vector merged; - for (auto& region : regions) { + merged.reserve(regions.size()); + for (const auto index : order) { + auto& region = regions[index]; if (!merged.empty() && region.begin < merged.back().end) { auto& previous = merged.back(); if (previous.mirror != nullptr || region.mirror != nullptr) { @@ -2890,9 +2971,14 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { void GuestBufferMemory::UploadFinish(bool addressable) { Require(prepared && !uploaded, "guest memory upload was not prepared"); - uploaded = true; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = std::chrono::steady_clock::now(); + for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + if (space != nullptr) { + for (const auto& region : space->base) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + } + uploaded = true; + for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); // 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 @@ -2922,6 +3008,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 @@ -3069,35 +3166,12 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto bytes = region.end - region.begin; const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (addressable ? VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT : 0u); - if (profile) { - // Why the region is copied rather than bound in place, totalled every 1000 uploads (under a - // mutex: prepare stages of several builds copy at once). - static std::mutex reasonsMutex; - static std::uint64_t uploads = 0, noImport = 0, noImportBytes = 0, misaligned = 0, misalignedBytes = 0, snapshotOnly = 0, snapshotBytes = 0; - static std::map> outside; - bool inImport = false; - if (context.hostImportAlignment != 0) { - auto& state = Imports(); - std::lock_guard lock(state.mutex); - inImport = findImport(state, region.begin, region.end) != nullptr; - } - std::lock_guard lock(reasonsMutex); - if (!region.hostBacked && !region.writable) { ++snapshotOnly; snapshotBytes += bytes; } - else if (inImport) { ++misaligned; misalignedBytes += bytes; } - else { - ++noImport; - noImportBytes += bytes; - auto& bucket = outside[region.begin >> 28u]; - ++bucket.first; - bucket.second += bytes; - } - if (++uploads % 1000 == 0) { - // 'misaligned in imports' are the ones the GPU did not copy: over APS5_GPU_COPY_MAX_KIB, - // sparse, APS5_CPU_COPIES, no recorder, or an import gone by UploadFinish. - AgcDriver::ProfilePrint_nid_no_patch("[buffers] %llu copied regions on the CPU: %llu snapshots (%.0f MiB), %llu misaligned in imports (%.0f MiB), %llu outside imports (%.0f MiB):", static_cast(uploads), static_cast(snapshotOnly), snapshotBytes / 1048576.0, static_cast(misaligned), misalignedBytes / 1048576.0, static_cast(noImport), noImportBytes / 1048576.0); - for (const auto& [granule, counts] : outside) AgcDriver::ProfilePrint_nid_no_patch(" 0x%llx0000000:%llu/%.0fMiB", static_cast(granule), static_cast(counts.first), counts.second / 1048576.0); - AgcDriver::ProfilePrint_nid_no_patch("\n"); - } + std::uint64_t readBytes = 0; + bool inImport = false; + if (profile && context.hostImportAlignment != 0) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + inImport = findImport(state, region.begin, region.end) != nullptr; } const auto allocateStart = std::chrono::steady_clock::now(); if (region.sparse && region.buffer == nullptr && (region.hostBacked || region.writable)) { @@ -3121,26 +3195,54 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { const bool written = region.writable && WritesOverlap(region.begin, static_cast(bytes)); if (!region.sparse) { GuestMemory::Read(region.begin, region.buffer->Bytes()); + readBytes += region.buffer->Bytes().size(); if (written) region.uploaded.assign(region.buffer->Bytes().begin(), region.buffer->Bytes().end()); } else { // `uploaded` holds the backed pieces back to back. const auto target = region.buffer->Bytes().first(static_cast(allocated)); - const bool zeroGaps = region.bufferEnd != 0; std::size_t cursor = 0; for (const auto& [first, last] : region.backed) { const auto at = static_cast(first - region.begin); - if (zeroGaps && at > cursor) std::memset(target.data() + cursor, 0, at - cursor); + if (at > cursor) std::memset(target.data() + cursor, 0, at - cursor); const auto piece = target.subspan(at, static_cast(last - first)); GuestMemory::Read(first, piece); + readBytes += piece.size(); if (written) region.uploaded.insert(region.uploaded.end(), piece.begin(), piece.end()); cursor = at + piece.size(); } - if (zeroGaps && cursor < target.size()) std::memset(target.data() + cursor, 0, target.size() - cursor); + if (cursor < target.size()) std::memset(target.data() + cursor, 0, target.size() - cursor); } } - else std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + else { + std::memcpy(region.buffer->Bytes().data(), region.snapshot.data(), region.snapshot.size()); + readBytes += region.snapshot.size(); + } region.snapshot.clear(); if (profile) readUs.fetch_add(microsecondsSince(readStart), std::memory_order_relaxed); + if (profile) { + static std::mutex reasonsMutex; + static std::uint64_t uploads = 0, noImport = 0, noImportBytes = 0, misaligned = 0, misalignedBytes = 0, snapshotOnly = 0, snapshotBytes = 0; + static std::uint64_t logicalBytes = 0, readTotalBytes = 0; + static std::map> outside; + std::lock_guard lock(reasonsMutex); + const auto bufferBytes = region.buffer->Bytes().size(); + logicalBytes += bytes; + readTotalBytes += readBytes; + if (!region.hostBacked && !region.writable) { ++snapshotOnly; snapshotBytes += bufferBytes; } + else if (inImport) { ++misaligned; misalignedBytes += bufferBytes; } + else { + ++noImport; + noImportBytes += bufferBytes; + auto& bucket = outside[region.begin >> 28u]; + ++bucket.first; + bucket.second += bufferBytes; + } + if (++uploads % 1000 == 0) { + AgcDriver::ProfilePrint_nid_no_patch("[buffers] %llu completed CPU-copy operations: logical spans %.0f MiB, buffer spans %.0f MiB, bytes read %.0f MiB; %llu snapshots (%.0f MiB), %llu misaligned in imports (%.0f MiB), %llu outside imports (%.0f MiB):", static_cast(uploads), logicalBytes / 1048576.0, (snapshotBytes + misalignedBytes + noImportBytes) / 1048576.0, readTotalBytes / 1048576.0, static_cast(snapshotOnly), snapshotBytes / 1048576.0, static_cast(misaligned), misalignedBytes / 1048576.0, static_cast(noImport), noImportBytes / 1048576.0); + for (const auto& [granule, counts] : outside) AgcDriver::ProfilePrint_nid_no_patch(" 0x%llx0000000:%llu/%.0fMiB", static_cast(granule), static_cast(counts.first), counts.second / 1048576.0); + AgcDriver::ProfilePrint_nid_no_patch("\n"); + } + } } void GuestBufferMemory::recordGpuCopies(std::span copies, bool addressable) { @@ -3444,6 +3546,21 @@ bool GuestBufferMemory::HasCopiedWrites() const { return false; } +bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t bytes) const { + if (bytes == 0) return false; + for (const auto& [begin, end] : writes) { + if (!(address < end && begin < address + bytes)) continue; + const auto* found = owner(begin); + if (found == nullptr) continue; + const auto& region = *found; + if (region.direct != nullptr) continue; + if (region.gpuCopy && (region.copiedBack || region.deviceLocal)) continue; + if (region.mirror != nullptr && !region.mirror->writable) continue; + return true; + } + return false; +} + void GuestBufferMemory::MarkDirectWrites() const { for (const auto& [begin, end] : writes) { const auto* found = owner(begin); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 743b469cda..1ee9499274 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -5,13 +5,17 @@ #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 +#include #include #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -43,7 +47,7 @@ void recordCommands(const Context& context, Record&& record) { class MultisampleTarget { public: - MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) { + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); @@ -105,8 +109,8 @@ public: MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - if (!cmask.TakeClear()) return; - const auto clear = fastClearColor(use); + VkClearColorValue clear{}; + if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -150,12 +154,14 @@ public: } Context context; + std::shared_ptr> active = std::make_shared>(true); ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; + MultisampledColorFill colorFill; private: void release() noexcept { @@ -171,10 +177,19 @@ struct GuardedRange { std::uint64_t address; std::uint64_t bytes; std::uint32_t samples; + std::shared_ptr> active; +}; + +class PrimedSharedMutex : public std::shared_mutex { +public: + PrimedSharedMutex() { + lock(); + unlock(); + } }; std::shared_mutex& guardedMutex() { - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; return mutex; } @@ -192,13 +207,35 @@ void guardReads(std::uint64_t address, std::size_t bytes) { RequireNoMultisampleTarget(address, bytes, "a CPU read"); } -void guard(const Context& context, const ColorTarget& target) { - const auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); +void retireUnmapped(std::uintptr_t address, std::size_t bytes) { std::unique_lock lock(guardedMutex()); - guarded().push_back({context.device, target.address, bytes, target.samples}); + 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); + const auto log2Pixels = 16u - std::countr_zero(target.elementBytes) - log2Samples; + const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; + const auto blockWidth = 1u << log2Width; + const auto blockHeight = 1u << (log2Pixels - log2Width); + const auto align = [](std::uint32_t extent, std::uint32_t block) { return (static_cast(extent) + block - 1u) / block * block; }; + bytes = align(target.extent.width, blockWidth) * align(target.extent.height, blockHeight) * target.elementBytes * target.samples; + } + std::unique_lock lock(guardedMutex()); + guarded().push_back({context.device, target.address, bytes, target.samples, active}); 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() { @@ -213,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; @@ -272,6 +310,71 @@ bool MultisampledCmask::TakeClear() { return true; } +std::uint64_t MultisampledColorBytes(const ColorTarget& target) { + const auto blockLog2 = target.tileMode == ColorTileMode::Standard4KB ? 12u : target.tileMode == ColorTileMode::Standard64KB || target.tileMode == ColorTileMode::RenderTarget ? 16u : 0u; + if (blockLog2 == 0 || !std::has_single_bit(target.elementBytes) || !std::has_single_bit(target.samples)) return 0; + const auto samplesLog2 = static_cast(std::countr_zero(target.samples)); + const auto elementsLog2 = blockLog2 - static_cast(std::countr_zero(target.elementBytes)) - samplesLog2; + const auto widthLog2 = (elementsLog2 + ((samplesLog2 & 1u) == 0 ? 1u : 0u)) / 2u; + const std::uint64_t blockWidth = std::uint64_t{1} << widthLog2; + const std::uint64_t blockHeight = std::uint64_t{1} << (elementsLog2 - widthLog2); + return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; +} + +bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { + const std::array pattern{fill, fill, fill, fill}; + VkFormat unorm = VK_FORMAT_UNDEFINED; + switch (target.format) { + case VK_FORMAT_R8G8B8A8_SRGB: unorm = VK_FORMAT_R8G8B8A8_UNORM; break; + case VK_FORMAT_B8G8R8A8_SRGB: unorm = VK_FORMAT_B8G8R8A8_UNORM; break; + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: unorm = VK_FORMAT_A8B8G8R8_UNORM_PACK32; break; + default: return ClearColorForTexel(target.format, target.elementBytes, pattern, clear); + } + if (!ClearColorForTexel(unorm, target.elementBytes, pattern, clear)) return false; + return std::all_of(clear.float32, clear.float32 + 3, [](float value) { return value == 0.0f || value == 1.0f; }); +} + +MultisampledColorFill::MultisampledColorFill(const ColorTarget& target) : address(target.address), bytes(MultisampledColorBytes(target)) {} + +void MultisampledColorFill::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (bytes == 0 ? fillAddress > address || address >= fillAddress + fillBytes : fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + char text[192]; + if (bytes == 0) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx covers the color memory at 0x%llx of a multisampled target whose footprint is not modeled", static_cast(fillAddress), fillBytes, static_cast(address)); + refusal = text; + return; + } + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the color memory 0x%llx+0x%llx of a multisampled target, leaving its samples mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + filled = pattern; +} + +std::optional MultisampledColorFill::TakeFill() { + Require(refusal.empty(), refusal); + return std::exchange(filled, std::nullopt); +} + +void MultisampledColorFill::Discard() { + refusal.clear(); + filled.reset(); +} + +bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear) { + if (cmask.TakeClear()) { + colorFill.Discard(); + clear = fastClearColor(use); + return true; + } + const auto fill = colorFill.TakeFill(); + if (!fill) return false; + Require(MultisampledFillClearColor(use, *fill, clear), "a fill over the color memory of a multisampled target has no exact clear value in its format"); + return true; +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); return acquire(context, target).view; @@ -302,6 +405,8 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, 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); if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; @@ -316,6 +421,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 0698091846..6eeefefa2a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -69,10 +69,11 @@ bool HasStencil(VkFormat format) { return format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_S8_UINT; } -Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent) : context(context) { +Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::span targets, VkExtent2D extent, std::uint32_t layers) : context(context) { // Cached objects outlive their device's teardown; they must not keep its buffer pool alive past it. this->context.bufferPool.reset(); Require(extent.width != 0 && extent.height != 0 && extent.width <= context.limits.maxFramebufferWidth && extent.height <= context.limits.maxFramebufferHeight, "framebuffer extent exceeds device limits"); + Require(layers != 0 && layers <= context.limits.maxFramebufferLayers, "framebuffer layers exceed device limits"); views.assign(targets.begin(), targets.end()); if (renderPass == VK_NULL_HANDLE) return; VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; @@ -81,7 +82,7 @@ Framebuffer::Framebuffer(const Context& context, VkRenderPass renderPass, std::s framebufferInfo.pAttachments = targets.empty() ? nullptr : targets.data(); framebufferInfo.width = extent.width; framebufferInfo.height = extent.height; - framebufferInfo.layers = 1; + framebufferInfo.layers = layers; Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer"); } @@ -116,7 +117,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp bool ValidateSampleLocations(const Context& context, const State& state) { const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0; Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); - const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, HostDepthFormat(context, state.depth->format), state.depth->samples)); Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); return enabled; } @@ -182,6 +183,13 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(key, shader.stage); appendKey(key, shader.program->PipelineVariantId()); appendKey(key, shader.pushConstantOffset); + if (resources.split) { + auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; + for (const auto& binding : shader.program->bindings) { + Require(binding.descriptorSet < 2u, "split pipeline library descriptor set exceeds two slots"); + sets |= 1u << binding.descriptorSet; + } + } } for (const auto& binding : input.bindings) { appendKey(keys.vertexInput, binding.binding); @@ -246,8 +254,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } }; splitKey(keys.layout, 3u); - splitKey(keys.preRasterizationLayout, 1u); - splitKey(keys.fragmentLayout, 2u); + splitKey(keys.preRasterizationLayout, keys.preRasterizationSets); + splitKey(keys.fragmentLayout, keys.fragmentSets); return true; } @@ -343,7 +351,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; if (state.depth) { VkAttachmentDescription depth{}; - depth.format = state.depth->format; + depth.format = HostDepthFormat(context, state.depth->format); depth.samples = static_cast(state.samples); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; @@ -360,11 +368,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); if (state.depth) { - rendering.depthAttachmentFormat = state.depth->format; - if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = state.depth->format; + rendering.depthAttachmentFormat = HostDepthFormat(context, state.depth->format); + if (HasStencil(state.depth->format)) rendering.stencilAttachmentFormat = rendering.depthAttachmentFormat; } if (dynamicRendering) { - depthFormat = state.depth ? state.depth->format : VK_FORMAT_UNDEFINED; + depthFormat = state.depth ? HostDepthFormat(context, state.depth->format) : VK_FORMAT_UNDEFINED; colorSlots.assign(state.blends.size(), ~0u); for (std::uint32_t index = 0; index < state.colors.size(); ++index) colorSlots.at(state.colors[index].exportIndex) = index; this->attachmentLayout = attachmentLayout; @@ -564,7 +572,7 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent) { +std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::uint32_t layers, std::span persistent) { Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); bool resident = true; for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0)); @@ -579,7 +587,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::spanextent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; + if (it->extent.width != extent.width || it->extent.height != extent.height || it->layers != layers || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; // View handles are recycled once a StorageTexture is destroyed, so the owners must be the // very objects the views were made for. bool same = true; @@ -589,7 +597,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(context, renderPass, targets, extent); + auto framebuffer = std::make_shared(context, renderPass, targets, extent, layers); if (!resident) return framebuffer; // Beyond the bound the least recently used unreferenced entry goes. constexpr std::size_t bound = 8; @@ -603,6 +611,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(colors.size()); rendering.pColorAttachments = colors.empty() ? nullptr : colors.data(); if (depthFormat != VK_FORMAT_UNDEFINED) rendering.pDepthAttachment = &depth; @@ -665,7 +674,7 @@ void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { setLibraryState(commands, state); return; } - if (depthBias) context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, state.depthBiasConstant, state.depthBiasClamp, state.depthBiasSlope); + if (depthBias) context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, HostDepthBiasConstant(context, state.depth->format, state.depthBiasConstant), state.depthBiasClamp, state.depthBiasSlope); if (!depthBounds) return; context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, state.minDepthBounds, state.maxDepthBounds); } @@ -674,7 +683,7 @@ void Pipeline::setLibraryState(VkCommandBuffer commands, const State& state) con const bool depth = state.depth.has_value(); context.Resolved(&DeviceFunctions::cmdSetCullMode, "vkCmdSetCullModeEXT")(commands, state.cullMode); context.Resolved(&DeviceFunctions::cmdSetFrontFace, "vkCmdSetFrontFaceEXT")(commands, state.frontFace); - context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, depthBias ? state.depthBiasConstant : 0.0f, depthBias ? state.depthBiasClamp : 0.0f, depthBias ? state.depthBiasSlope : 0.0f); + context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, depthBias ? HostDepthBiasConstant(context, state.depth->format, state.depthBiasConstant) : 0.0f, depthBias ? state.depthBiasClamp : 0.0f, depthBias ? state.depthBiasSlope : 0.0f); context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, depthBounds ? state.minDepthBounds : 0.0f, depthBounds ? state.maxDepthBounds : 1.0f); context.Resolved(&DeviceFunctions::cmdSetDepthTestEnable, "vkCmdSetDepthTestEnableEXT")(commands, depth && state.depthTest); context.Resolved(&DeviceFunctions::cmdSetDepthWriteEnable, "vkCmdSetDepthWriteEnableEXT")(commands, depth && state.depthWrite); @@ -701,7 +710,7 @@ void Pipeline::ContinueBackFaces(VkCommandBuffer commands, const State& state) c context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, backFaces); context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &state.scissor); - context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, state.backDepthBiasConstant, state.depthBiasClamp, state.backDepthBiasSlope); + context.Resolved(&DeviceFunctions::cmdSetDepthBias, "vkCmdSetDepthBias")(commands, HostDepthBiasConstant(context, state.depth->format, state.backDepthBiasConstant), state.depthBiasClamp, state.backDepthBiasSlope); if (depthBounds) context.Resolved(&DeviceFunctions::cmdSetDepthBounds, "vkCmdSetDepthBounds")(commands, state.minDepthBounds, state.maxDepthBounds); } @@ -818,6 +827,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.domain); append(key, tessellation.partitioning); append(key, tessellation.outputTopology); + append(key, tessellation.patchesPerGroup); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index db91349596..bfdb538df6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -104,6 +104,8 @@ std::span PipelineLibraryDynamicStates() { VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized, bool partsOnly, std::span modules) { Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states"); + const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; + Require(!split || (layout.setLayoutCount == 2u && layout.pSetLayouts != nullptr && ((keys.preRasterizationSets | keys.fragmentSets) & ~3u) == 0u), "split pipeline library layout requires two descriptor set slots and a valid mask"); auto& store = Libraries(); std::unique_lock lock(store.mutex); auto& device = store.devices[context.device]; @@ -112,22 +114,23 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip device.destroyLayout = context.Function("vkDestroyPipelineLayout"); device.destroyModule = context.Function("vkDestroyShaderModule"); } - const auto layoutFor = [&](const Key& key, std::optional set) { + const auto layoutFor = [&](const Key& key, std::optional mask) { auto found = device.layouts.find(key); if (found != device.layouts.end()) return found->second; auto info = layout; std::array sets{VK_NULL_HANDLE, VK_NULL_HANDLE}; - if (set) { - sets.at(*set) = layout.pSetLayouts[*set]; + if (mask.has_value()) { + for (std::uint32_t set = 0; set < sets.size(); ++set) { + if ((*mask & (1u << set)) != 0u) sets[set] = layout.pSetLayouts[set]; + } info.pSetLayouts = sets.data(); } VkPipelineLayout handle = VK_NULL_HANDLE; Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library"); return device.layouts.emplace(key, handle).first->second; }; - const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0; const auto fullLayout = layoutFor(keys.layout, std::nullopt); - const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE}; + const std::array partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, keys.preRasterizationSets) : fullLayout, split ? layoutFor(keys.fragmentLayout, keys.fragmentSets) : fullLayout, VK_NULL_HANDLE}; std::vector preRasterization; const VkPipelineShaderStageCreateInfo* fragment = nullptr; bool mesh = false; @@ -146,8 +149,16 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip }; const std::array partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput}; const Key none; - const std::array contextKeys{&none, &keys.preRasterizationLayout, &keys.fragmentLayout, &keys.renderPass}; - const VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; + VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; + shaderRendering.viewMask = rendering.viewMask; + shaderRendering.depthAttachmentFormat = rendering.depthAttachmentFormat; + shaderRendering.stencilAttachmentFormat = rendering.stencilAttachmentFormat; + const std::array attachments{shaderRendering.viewMask, static_cast(shaderRendering.depthAttachmentFormat), static_cast(shaderRendering.stencilAttachmentFormat)}; + const auto attachmentBytes = std::as_bytes(std::span(attachments)); + const Key attachmentKey(attachmentBytes.begin(), attachmentBytes.end()); + const auto preRasterizationContext = Combined(keys.preRasterizationLayout, attachmentKey); + const auto fragmentContext = Combined(keys.fragmentLayout, attachmentKey); + const std::array contextKeys{&none, &preRasterizationContext, &fragmentContext, &keys.renderPass}; VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; const bool optimize = backgroundLinking && context.pipelineLinkTimeOptimization; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index ea8d595f59..192d1e9c3c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1811,7 +1811,7 @@ bool Recorder::GpuTimingEnabled() { } bool Recorder::BatchStampsEnabled() { - return GpuTimingEnabled() || DrawProfiled(); + return GpuTimingEnabled() || DrawProfiled() || PerformanceTrace::Configured(); } void Recorder::CountBarriers(CommandClass which, std::uint32_t count) { @@ -1903,7 +1903,7 @@ std::uint32_t Recorder::BeginGpuTiming(std::uint64_t key) { std::uint32_t Recorder::beginTiming(std::uint64_t key) { const bool full = GpuTimingEnabled(); - if (!full && !(key == BatchTimingKey && DrawProfiled())) return NoTiming; + if (!full && !(key == BatchTimingKey && BatchStampsEnabled())) return NoTiming; const auto commands = open->commands; const std::uint32_t queryCount = full ? MaxTimedRanges * 2 : 2; if (open->queries == VK_NULL_HANDLE) { @@ -2358,6 +2358,7 @@ void Recorder::readGpuTiming(Batch& batch) { if (batch.batchTiming == 0 && stamps[1] >= stamps[0]) { batch.gpuStartNs = static_cast(stamps[0]) * period; batch.gpuEndNs = static_cast(stamps[1]) * period; + PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs); } return; } @@ -2373,10 +2374,12 @@ void Recorder::readGpuTiming(Batch& batch) { // The whole batch: apart from the per-program totals it contains. batch.gpuStartNs = static_cast(stamps[i * 2]) * period; batch.gpuEndNs = static_cast(stamps[i * 2 + 1]) * period; + PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs); timingBatchMs += ns / 1e6; continue; } const auto key = batch.timedKeys[i]; + if (key >= ClassKey(CommandClass::DispatchLeading) && key < ClassKey(CommandClass::Count)) PerformanceTrace::GpuRange(CommandClassNames[key - ClassKey(CommandClass::DispatchLeading)], static_cast(stamps[i * 2]) * period, static_cast(stamps[i * 2 + 1]) * period); auto& totals = timingByKey[key]; ++totals.count; totals.ms += ns / 1e6; @@ -3306,9 +3309,6 @@ void Recorder::SyncThrough(std::uint64_t address, std::size_t bytes, bool waitUn // later batch's own ordering against this write-back is its recorder's business at record // time (FillBuffer and DispatchIndirect consult copiedWriters; a later copied writer's // write-back runs after this one in finish order; a label behind completions lands after). - // Known gap, not introduced here: a later batch writing the same range GPU-direct in place - // (a writable region bound in place, or a GPU copy-back) is not ordered against this - // write-back by the resource build; it went from deterministically stale to racy. announcedSource = 4; announcedSite = nullptr; ++holdCounters.completionSyncsSkipped; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 61cfa6b420..df4bfe5fe8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -143,6 +143,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; + const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ @@ -164,7 +165,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable, + .earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 63bd7b18ec..552145a607 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include #include #include @@ -572,6 +573,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spanbytes); }; - if (*keys != DccKeys::Uncompressed || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { + if ((*keys != DccKeys::Uncompressed && *keys != DccKeys::ClearSingle) || GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation) || equalsCommitted()) { it->generation = generation; touchTexture(cache, it); logLookup({it->texture.get(), resource, guestBytes, *keys, generation, nullptr}); @@ -837,11 +839,7 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); - if (DepthSurfaceAt(context, words, viewed)) { - 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(viewed.baseAddress), viewed.width, viewed.height, viewed.format, mip, static_cast(viewed.dimension), static_cast(viewed.tileMode)); - throw std::runtime_error(text); - } + if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); @@ -1717,6 +1715,7 @@ void ShaderResources::noteReusable() { reusable = false; directRegions.clear(); if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return; + if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); if (!regions.has_value()) return; @@ -2290,13 +2289,39 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span return OwnRefreshFallback::Count; } +void CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || Recorder::InCompletion()) return; + auto* recorder = Recorder::Active(); + if (recorder == nullptr) return; + const auto drawWriters = DrawCopiedWriters(); + const bool labels = Recorder::PendingCompletionLabels() != 0; + if ((context.copiedWriters == nullptr || context.copiedWriters->empty()) && drawWriters->empty() && !labels) return; + if (!recorder->PendingWriteOverlaps(address, bytes)) return; + const auto overlaps = [&](const auto& writers) { + return std::any_of(writers.begin(), writers.end(), [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }); + }; + if ((context.copiedWriters != nullptr && overlaps(*context.copiedWriters)) || overlaps(*drawWriters) || (labels && recorder->PendingLabelIn(address, bytes))) { + Recorder::CountSync(4); + recorder->SyncThrough(address, bytes); + } +} + bool ShaderResources::Revalidate(std::span shaders, ProofReport* report) { if (report != nullptr) *report = {ProofPath::Full, ProofFailure::Other}; if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + for (const auto& range : describedRanges) { + CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); + if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes)); + } + for (const auto& image : storageTextures) { + CompletePendingCpuWrites(context, image->Descriptor().baseAddress, static_cast(image->GuestBytes())); + if (image->Descriptor().dccAddress != 0) CompletePendingCpuWrites(context, image->Descriptor().dccAddress, DccKeyBytes(image->GuestBytes())); + } // APS5_NO_FAST_REVALIDATE=1 always repeats the lookups. static const bool noFast = std::getenv("APS5_NO_FAST_REVALIDATE") != nullptr; - const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; const auto finish = [&](bool fast, bool ok, ProofFailure failure = ProofFailure::Other) { if (profile) countRevalidate(fast, ok, start); if (report != nullptr) report->failure = ok ? ProofFailure::None : failure; @@ -2430,11 +2455,12 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR return true; }; if (EpochRevalidate()) { - if (!directRegions.empty() && !(pendingSerialSeen != 0 && pendingSerialSeen == StorageTexture::PendingSerial())) { + if ((!directRegions.empty() || !guestMemory.Writes().empty()) && !(pendingSerialSeen != 0 && pendingSerialSeen == StorageTexture::PendingSerial())) { thread_local std::vector* regionsSlot = nullptr; auto& regions = ShaderRecompiler::ThreadOwned(regionsSlot); regions.clear(); for (const auto& region : directRegions) regions.push_back({region.begin, region.end, nullptr, nullptr, false}); + for (const auto& [begin, end] : guestMemory.Writes()) regions.push_back({begin, end, nullptr, nullptr, false}); StorageTexture::ScanPending(regions); // A region over a unit shadow's fresh results (a retile bumped the serial) is // published by the flush, as one over a pending image is stored. @@ -2450,6 +2476,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } } else { for (const auto& region : directRegions) StorageTexture::FlushPending(region.begin, static_cast(region.end - region.begin), nullptr, "imported buffer region"); + for (const auto& [begin, end] : guestMemory.Writes()) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (!serialLoop()) return finish(fast, false, ProofFailure::Imports); } // (3) Buffers staged in device memory (GuestBufferMemory::AllowDeviceStaging) are copied in @@ -3194,7 +3221,7 @@ bool ShaderResources::precollectImages() { } std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) { - if (record.texture == nullptr) return nullptr; + if (record.texture == nullptr || record.texture->RefreshedPerUse()) return nullptr; struct Outcome { bool profile; std::chrono::steady_clock::time_point start; @@ -3233,7 +3260,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) // Under fast-clear keys the view holds only while its image's results are still pending over // the surface; flushed, the clear the image cannot see makes the lookup take a snapshot. if (keys != DccKeys::Uncompressed && StorageTexture::FindPending(address, record.guestBytes) != record.source) return nullptr; - } else if (keys == DccKeys::Uncompressed && !GuestMemory::UnchangedSince(address, bytes, record.entryGeneration)) { + } else if ((keys == DccKeys::Uncompressed || keys == DccKeys::ClearSingle) && !GuestMemory::UnchangedSince(address, bytes, record.entryGeneration)) { return nullptr; } // The entry must still be the cache's, holding this object (an evicted object is not reused: the @@ -3287,6 +3314,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; + CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); + if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; @@ -3351,6 +3380,8 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u); Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented"); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; + CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); + if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); // The same surface as the previous element: its image was just looked up and refreshed. if (sameAsPrevious && StorageDedupeEnabled()) storageTextures.push_back(storageTextures.back()); else storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes)); @@ -3612,7 +3643,9 @@ void ShaderResources::WriteBack() { WriteBackBuffers(); if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (storageWritten[index]) storageTextures[index]->MarkDirty(); + if (!storageWritten[index]) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } } @@ -3623,7 +3656,9 @@ void ShaderResources::MarkGpuWrites(Recorder& recorder) { recorder.NotePendingReads(guestMemory.InPlaceReads(), guestMemory.HoldsLease() ? Recorder::ReadKind::AddressBased : Recorder::ReadKind::DispatchElement); if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (storageWritten[index]) storageTextures[index]->MarkDirty(); + if (!storageWritten[index]) continue; + storageTextures[index]->MarkDirty(); + static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } // Written sub-ranges of buffers the GPU copied out of a host import go back into it by the GPU, // recorded here after the work: those regions then need no CPU write-back (HasCopiedWrites), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 4022508ff1..25148b44ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include #include +#include #include #include #include @@ -21,6 +22,7 @@ struct Decoration { std::optional stride; std::optional index; bool block = false; + bool aliased = false; bool patch = false; bool perPrimitive = false; bool perVertex = false; @@ -43,9 +45,11 @@ struct Module { std::map outputs; std::optional secondSource; bool position = false; - bool clipDistance = false; + std::uint32_t clipDistances = 0; + std::uint32_t cullDistances = 0; bool primitiveIndices = false; bool fragmentBarycentric = false; + bool layerOutput = false; std::map> modes; const std::vector& Type(std::uint32_t id) const { @@ -110,9 +114,13 @@ struct Module { if (!input) position = true; return; } - if (value == spv::BuiltInClipDistance && !input && (vertex || stage == Stage::TessellationEvaluation)) { - Require(!clipDistance && Signature(type) == "f32[1]", "invalid or duplicate clip distance output"); - clipDistance = true; + // 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))) { + 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"); + count = static_cast(signature[4] - '0'); + Require(clipDistances + cullDistances <= 8, "more than eight clip and cull distances in one shader"); return; } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { @@ -127,13 +135,14 @@ struct Module { return; } if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = true; - Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool"), "unsupported mesh built-in"); + Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in"); return; } const auto signature = Signature(type); if (vertex && storage == spv::StorageClassInput) { Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { + if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { @@ -142,7 +151,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -192,6 +201,11 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragmentShaderBarycentric, "device does not support enabled VK_KHR_fragment_shader_barycentric with fragmentShaderBarycentric"); module.fragmentBarycentric = true; } + const bool isLayerCapability = capability == spv::CapabilityShaderViewportIndexLayerEXT; + if (isLayerCapability) { + Require(viewportIndexLayer, "device does not support enabled VK_EXT_shader_viewport_index_layer"); + module.layerOutput = true; + } VkSubgroupFeatureFlags subgroupOperations = 0; switch (capability) { case spv::CapabilityGroupNonUniform: subgroupOperations = VK_SUBGROUP_FEATURE_BASIC_BIT; break; @@ -236,6 +250,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; + if ((vertex || evaluation) && (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 + ")"); + } + // Enabled unconditionally or by the device setup in VulkanDevice. const bool isFeatureCapability = capability == spv::CapabilitySampled1D || @@ -247,6 +267,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || capability == spv::CapabilityStorageImageMultisample || + (vertex && shaderCullDistance && capability == spv::CapabilityCullDistance) || capability == spv::CapabilityStorageImageWriteWithoutFormat || capability == spv::CapabilityStorageImageReadWithoutFormat || capability == spv::CapabilityInt64 || @@ -281,7 +302,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi isFeatureCapability || isDescriptorIndexingCapability || ((imageInt64Atomics || bufferInt64Atomics) && capability == spv::CapabilityInt64Atomics) || - (imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT), + (imageInt64Atomics && capability == spv::CapabilityInt64ImageEXT) || + isLayerCapability, std::string("SPIR-V requires unsupported device capability ") + std::to_string(static_cast(capability))); @@ -306,7 +328,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } - Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); + Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } case spv::OpDecorateId: @@ -363,6 +385,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi decoration.perVertex = true; } if (kind == spv::DecorationBlock) decoration.block = true; + if (kind == spv::DecorationAliased) decoration.aliased = true; break; } case spv::OpMemberDecorate: @@ -418,9 +441,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints}); Require(module.modes.size() == 1, "unsupported tessellation-control execution mode"); } else if (evaluation) { - mode(state.rectList ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles, {}); + Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); + 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(spv::ExecutionModeVertexOrderCw, {}); + mode(counterClockwise ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (geometry) { mode(spv::ExecutionModeTriangles, {}); @@ -432,6 +458,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthReplacing || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater || name == spv::ExecutionModePixelInterlockOrderedEXT || name == spv::ExecutionModeStencilRefReplacingEXT), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; + std::map, bool> bufferViews; bool push = false; for (const auto& [id, variable] : module.variables) { if (variable.storage == spv::StorageClassFunction || variable.storage == spv::StorageClassPrivate) continue; @@ -490,7 +517,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } } } else if (variable.storage == spv::StorageClassPushConstant) { - Require(!push && (!shader.pushConstants.empty() || mesh) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); + Require(!push && (!shader.pushConstants.empty() || mesh || (control && !rectListControl)) && (type[0] & 0xffffu) == spv::OpTypeStruct, "invalid push constant interface"); push = true; Require(type.size() == 3 && module.decorations[typeId].block, "push constant variable must be a Block struct with exactly one member"); const auto offset = module.offsets.find({typeId, 0}); @@ -518,12 +545,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else { Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); - Require(descriptors.insert(key).second, "duplicate SPIR-V resource binding"); const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); + const bool array = binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; + const auto [view, first] = bufferViews.emplace(key, decoration.aliased); + Require(descriptors.insert(key).second || (!first && view->second && decoration.aliased && array), "duplicate SPIR-V resource binding"); Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata"); Require(binding->kind == ShaderRecompiler::DescriptorKind::StorageBuffer, "SPIR-V descriptor type disagrees with recompiler binding metadata"); Require(!binding->readOnly, "read-only descriptor metadata is unsupported because the recompiler emits no NonWritable decoration"); - const bool array = binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; Require(array || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (binding->role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding->role == ShaderRecompiler::DescriptorRole::FaultBuffer)) || binding->role == ShaderRecompiler::DescriptorRole::ShaderData || binding->role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding->role == ShaderRecompiler::DescriptorRole::Gds, "SPIR-V descriptor role is unsupported"); auto blockId = typeId; if (array) { @@ -537,7 +565,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto& block = module.Type(blockId); Require(block.size() == 3 && (block[0] & 0xffffu) == spv::OpTypeStruct && module.decorations[blockId].block, "descriptor must be a Block struct with exactly one member"); const auto& words = module.Type(block[2]); - Require(words.size() == 3 && (words[0] & 0xffffu) == spv::OpTypeRuntimeArray && module.Signature(words[2]) == "u32", "descriptor block member must be a runtime array of u32"); + Require(words.size() == 3 && (words[0] & 0xffffu) == spv::OpTypeRuntimeArray, "descriptor block member must be a runtime array"); + const auto& element = module.Type(words[2]); + const bool unsignedInteger = element.size() == 4 && (element[0] & 0xffffu) == spv::OpTypeInt && element[3] == 0u; + Require(unsignedInteger && (element[2] == 32u || (array && decoration.aliased && element[2] == 64u)), "descriptor block member must be a runtime array of u32, or of u64 in an aliased guest buffer view"); const auto member = module.offsets.find({blockId, 0}); Require(member != module.offsets.end() && member->second == 0, "descriptor block member must have offset zero"); } @@ -554,7 +585,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage"); } Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); - Require(push == (!shader.pushConstants.empty() || mesh), "recompiler push constant metadata disagrees with SPIR-V"); + Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); @@ -563,7 +594,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -584,7 +615,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a5f5d44072..8a07f6bc87 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -2,7 +2,9 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/General.hpp" #include @@ -97,9 +99,8 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt // The register rules DecodeState and DrawRejection share (the precheck must reject exactly what // DecodeState would): the masks whose set bits are unsupported, and the depth-control verdict. -// Render target index, viewport index and the misc export vector that carries them are accepted but -// not routed: color targets are single-layer, so layered draws land in layer 0. constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | (1u << 24u); +constexpr std::uint32_t ClipCullExports = 0xffffu | (1u << 22u) | (1u << 23u); 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. @@ -354,9 +355,20 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { return {}; } +constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; +constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); + +bool coveredCentroidSupported(const QueueState& queue) { + const auto config = find(queue.context, 0x2f8); + if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); - zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); + zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); const auto log2 = config & 7u; if (log2 == 0) { Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); @@ -612,11 +624,14 @@ 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}; + ShaderRecompiler::TessellationConfiguration tess{(config >> 8u) & 0x3fu, (config >> 14u) & 0x3fu, parameters & 3u, (parameters >> 2u) & 7u, (parameters >> 5u) & 7u, config & 0xffu}; 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.partitioning == 2 && tess.outputTopology == 2, "only triangular, fractional-odd, clockwise tessellation is supported by the reference path"); - Require(readFloat(queue.context, 0x287) <= 1.0f && readFloat(queue.context, 0x286) >= 63.0f, "tessellation level clamps inside the fractional-odd range [1, 63] are unsupported"); + 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"); + 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"); @@ -677,6 +692,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { switch (primitive) { case 1: Require(result.stages.mesh.has_value(), "point-list vertex rendering requires point-size output support"); result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break; case 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break; + case 3: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break; case 7: case 17: Require(result.stages.path == ShaderPath::Vertex, "rect-list requires vertex routing"); @@ -691,15 +707,11 @@ 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; - if ((read(cx, 0x207) & LayerExports) != 0) { - static bool reported = false; - if (!reported) { - reported = true; - std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207)); - } - } - zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + zero(cx, 0x207, ~(LayerExports | ClipCullExports), "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); + Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); @@ -751,7 +763,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto raster = read(cx, 0x205); // Bits 5-10 give the front/back polygon type (2 = filled triangles), which POLY_MODE (bit 3) turns on // explicitly; KEEP_TOGETHER_ENABLE (bit 24) only affects primitive distribution across the chip. - const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; const auto lineOffset = (raster & 0x1800u) != 0 && !pointsOrLines ? 0x2000u : 0u; const auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias or nonstandard rasterization")); @@ -813,6 +825,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } if (result.hasColorTarget) { result.color = result.colors.front(); + result.renderLayers = result.color.layers; + for (const auto& color : result.colors) Require(color.layers == result.renderLayers, "color targets with different layer counts are unsupported"); + if (result.renderLayers > 1) { + Require(!result.depth, "layered rendering with a depth target is unsupported"); + result.layerExports = read(cx, 0x207) & (1u << 18u); + } if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)}; } else if (result.depth) { result.renderExtent = result.depth->extent; @@ -823,6 +841,13 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } + if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { + static bool reported = false; + if (!reported) { + reported = true; + std::fprintf(stderr, "[gpu] layer/viewport index vertex exports are ignored (PA_CL_VS_OUT_CNTL=0x%08x)\n", read(cx, 0x207)); + } + } const auto xs = readFloat(cx, 0x10f); const auto xo = readFloat(cx, 0x110); const auto ys = readFloat(cx, 0x111); @@ -951,6 +976,17 @@ std::array ExportPackings(const State& return packings; } +ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { + if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; + switch (state.topology) { + case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return ShaderRecompiler::RasterPrimitive::Point; + case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: + case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: return ShaderRecompiler::RasterPrimitive::Line; + default: return ShaderRecompiler::RasterPrimitive::Triangle; + } +} + std::array ExportMappings(const State& state) { std::array mappings{}; mappings.fill(0xe4u); @@ -975,6 +1011,41 @@ std::uint32_t ColorWriteMask(const Registers& context) { return mask; } +namespace { + +std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { + if (const auto guest = FindGuestColorTargetFormat(format, elementBytes)) return *guest; + throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast(format))); +} + +} + +GuestTextureResource SurfaceForTarget(const ColorTarget& color) { + Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); + const bool chain = color.mipCount > 1; + GuestTextureResource surface{}; + surface.baseAddress = chain ? color.surfaceAddress : color.address; + surface.pipeBankXor = color.pipeBankXor; + surface.width = chain ? color.surfaceExtent.width : color.extent.width; + surface.height = chain ? color.surfaceExtent.height : color.extent.height; + surface.depthOrLastArray = color.depth - 1u; + surface.baseArray = 0; + surface.mipCount = color.mipCount; + surface.baseLevel = 0; + surface.lastLevel = color.mipCount - 1; + surface.tileMode = ColorTextureTileMode(color.tileMode); + surface.dimension = color.depth > 1 ? TextureDimension::k3D : TextureDimension::k2D; + surface.format = GuestFormatFor(color.format, color.elementBytes); + surface.dstSelX = 4; + surface.dstSelY = 5; + surface.dstSelZ = 6; + surface.dstSelW = 7; + surface.dccAddress = color.dccAddress; + surface.dccAlphaOnMsb = color.dccAlphaOnMsb; + surface.dccPipeAligned = color.dccPipeAligned; + return surface; +} + ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto stride = slot * 0xfu; ColorTarget color{}; @@ -998,7 +1069,8 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; - Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported"); + const auto lastSlice = (view >> 13u) & 0x1fffu; + Require(lastSlice >= slice, "the color view ends before its first slice"); const auto viewMip = (view >> 26u) & 0xfu; const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; @@ -1010,14 +1082,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported"); if (volume) { color.depth = (attrib3 & 0x1fffu) + 1u; - Require(maxMip == 0 && (info & 0x10000000u) == 0, "mipmapped or DCC 3D color targets are unsupported"); - Require(slice < color.depth, "the color view slice is beyond the 3D surface"); + Require(maxMip == 0, "mipmapped 3D color targets are unsupported"); + Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface"); color.depthSlice = slice; + color.layers = std::min(lastSlice + 1u, color.depth) - slice; } else if ((attrib3 & 0x1fffu) != 0) { - Require(slice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface"); + Require(lastSlice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface"); color.arrayLastSlice = attrib3 & 0x1fffu; color.arraySlice = slice; } + if (!volume && lastSlice != slice) { + const auto exports = find(cx, 0x207); + Require(exports == cx.end() || (exports->second & (1u << 18u)) == 0, "layered draws into a color view of several array slices are unsupported"); + } color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; std::uint64_t mipOffset = 0; @@ -1040,8 +1117,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.pipeBankXor = static_cast(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u)); color.surfaceAddress -= color.pipeBankXor; } + Require(color.tileMode != ColorTileMode::D64KBX || (color.pipeBankXor & ~0xcf00u) == 0, "unsupported display color pipe/bank XOR column bits"); color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0; - if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + if (slice != 0 && !volume) { + color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); + if (color.arrayLastSlice == 0 && ColorTileModeIsXor(color.tileMode)) { + const auto* equation = FindTextureSwizzleEquation(static_cast(color.tileMode), color.elementBytes); + Require(equation != nullptr, "a sliced color target has no swizzle equation"); + const auto coordinates = slice << 24u; + for (std::uint32_t bit = 0; bit < equation->bits.size(); ++bit) { + color.pipeBankXor ^= (std::popcount(equation->bits[bit] & coordinates) & 1u) << bit; + } + } + } color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); @@ -1082,6 +1170,10 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { } } } + if (volume && (color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB)) { + color.bytes = DescribeSurface(SurfaceForTarget(color)).guestBytes; + GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); + } return color; } @@ -1091,12 +1183,15 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (control == cx.end()) return std::nullopt; const auto mode = (control->second >> 4u) & 7u; const auto aaConfig = find(cx, 0x2f8); - const bool resolve = mode == 3u && aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + const bool rasterizerSamples = aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + const auto sourceAttrib = find(cx, 0x31d); + const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; + const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); 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 samples = resolve ? decodeSamples(cx) : 1u; + const auto samples = resolve && rasterizerSamples ? decodeSamples(cx) : 1u; if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); const auto viewportControl = read(cx, 0x206); @@ -1120,7 +1215,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue if (resolve) { const auto source = DecodeColorBuffer(cx, 0); const auto destination = DecodeColorBuffer(cx, 1); - Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); + Require(samples == 1 || source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); @@ -1164,7 +1259,8 @@ 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, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports), "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); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; @@ -1189,7 +1285,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { 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 (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + 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"; const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason; if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index b6072c6750..c6bed0bba9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -475,7 +475,7 @@ bool Texture::CanDepthCopyFrom(const StorageTexture& source, const GuestTextureR return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; } -Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag) : context(context), storageSource(source), depthCopy(true) { +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"); const auto colorFormat = ResolveTextureFormat(descriptor.format); @@ -535,7 +535,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& } bool Texture::RefreshDepthCopy() { - if (!depthCopy || storageSource == nullptr) return false; + if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; if (copiedOnce && storageSource->Version() == copiedVersion) return false; 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; @@ -660,17 +660,22 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType) : context(context) { layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = depthImage; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = viewType; viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; + viewInfo.subresourceRange = {aspect, 0, 1, 0, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.subresourceRange.layerCount = 1; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView depth plane first layer"); + firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; } Texture::~Texture() { @@ -808,7 +813,6 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat) { } } -// Integer formats take integer clear values; the DCC clear codes are only mapped for the others. 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: @@ -823,9 +827,12 @@ bool IntegerFormat(VkFormat format) { } } -// The clear value a DCC clear code stands for, for non-integer formats. bool ClearColorFor(VkFormat format, DccKeys keys, VkClearColorValue& clear) { - if (IntegerFormat(format)) return false; + if (IntegerFormat(format)) { + if (keys != DccKeys::Clear0000) return false; + clear.uint32[0] = clear.uint32[1] = clear.uint32[2] = clear.uint32[3] = 0u; + return true; + } switch (keys) { case DccKeys::Clear0000: clear.float32[0] = clear.float32[1] = clear.float32[2] = clear.float32[3] = 0.0f; return true; case DccKeys::Clear0001: clear.float32[0] = clear.float32[1] = clear.float32[2] = 0.0f; clear.float32[3] = 1.0f; return true; @@ -912,6 +919,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers; layerGeneration.assign(trackedLayers, 0); layerPending.assign(trackedLayers, false); + layerOriginal.assign(trackedLayers, true); sliceLinearBytes = geometry.sliceLinearBytes; const auto linearBytes = sliceLinearBytes * arrayLayers; @@ -1326,26 +1334,26 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layers, std::uint32_t layer) { Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; - Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); + Require((depthSlice == 0 && layers == 1) || (volume && mip == 0 && layers != 0 && depthSlice + layers - 1u <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); - const auto found = attachmentViews.find({format, mip, depthSlice, layer}); + const auto found = attachmentViews.find({format, mip, depthSlice, layers, layer}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.pNext = &usage; viewInfo.image = image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : 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, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, layers}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created); + attachmentViews.emplace(std::tuple{format, mip, depthSlice, layers, layer}, created); return created; } @@ -1523,6 +1531,8 @@ LookupOutcomes& ThreadLookupOutcomes() { bool StorageTexture::Refresh() { const bool profile = LookupOutcomes::Profiled(); auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); + if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); if (refreshProved()) { NoteProved(); refreshesProved.fetch_add(1, std::memory_order_relaxed); @@ -1552,7 +1562,7 @@ bool StorageTexture::Refresh() { if (!GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), alias->layerGeneration, aliasStamped)) alias = nullptr; for (std::uint32_t unit = 0; alias != nullptr && unit < trackedLayers; ++unit) { if (!alias->layerPending[unit]) continue; - if (alias->layerGeneration[unit] == 0 || aliasStamped[unit] == GuestMemory::BlockMaybeWritten || (aliasStamped[unit] == GuestMemory::BlockUnchanged && layerPending[unit])) { + if (alias->layerGeneration[unit] == 0 || (aliasStamped[unit] == GuestMemory::BlockUnchanged && layerPending[unit])) { alias = nullptr; break; } @@ -1593,13 +1603,6 @@ bool StorageTexture::Refresh() { stampedBlocks.assign(generations.size(), 0); cpuBlocks.assign(generations.size(), 0); tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stampedBlocks, cpuBlocks); - for (std::uint32_t unit = 0; tracked && blockUnits && unit < trackedLayers; ++unit) { - if (stampedBlocks[unit] != GuestMemory::BlockMaybeWritten) continue; - std::array block{}; - if (!compareUntracked(layerBegin(unit), static_cast(layerBytes(unit)), block) || block[0] != GuestMemory::BlockUnchanged) continue; - stampedBlocks[unit] = GuestMemory::BlockUnchanged; - cpuBlocks[unit] = 0; - } compared = !tracked && compareUntracked(descriptor.baseAddress, static_cast(guestBytes), stampedBlocks, true); if (compared) cpuBlocks = stampedBlocks; }; @@ -1655,7 +1658,7 @@ bool StorageTexture::Refresh() { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (changed[layer] && layerPending[layer]) pendingChanged = true; } - if (!unchanged && !keysChanged && !pendingChanged && originalValid && equalsOriginal()) { + if (!unchanged && !keysChanged && !pendingChanged && originalHolds(descriptor.baseAddress, static_cast(guestBytes)) && equalsOriginal()) { unchanged = true; stamped = false; } @@ -1698,7 +1701,7 @@ bool StorageTexture::Refresh() { static const bool traceUpload = std::getenv("APS5_TRACE_UPLOAD") != nullptr; if (traceUpload) { const auto keys = TextureClearKeys(descriptor, guestBytes); - std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalValid ? 1 : 0, dirty ? 1 : 0); + std::fprintf(stderr, "[upload] 0x%llx+0x%llx %ux%u mips %u: %s (keys %s -> %s, originalValid %d, dirty %d)\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), descriptor.width, descriptor.height, descriptor.mipCount, keys != uploadedKeys ? "DCC keys changed" : "guest memory changed", DccKeysName(uploadedKeys), DccKeysName(keys), originalHolds(descriptor.baseAddress, static_cast(guestBytes)) ? 1 : 0, dirty ? 1 : 0); } // Changed layers with results pending: the CPU wrote this memory while GPU results were // pending, so as with an immediate write-back followed by the CPU write, its blocks win and the @@ -1730,7 +1733,7 @@ bool StorageTexture::Refresh() { // drop rule. Debug aid: APS5_KEYFLIP_DROP=1 drops as before. static const bool keyFlipDrop = std::getenv("APS5_KEYFLIP_DROP") != nullptr; static const bool traceKeys = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr; - const bool keyFlip = keysChanged && blockUnits && !keyFlipDrop && IsDccClear(uploadedKeys) && keys == DccKeys::Uncompressed && stampedBlocks.size() >= trackedLayers; + const bool keyFlip = keysChanged && blockUnits && !keyFlipDrop && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle) && keys == DccKeys::Uncompressed && stampedBlocks.size() >= trackedLayers; if (traceKeys && keysChanged) { std::size_t pendingUnits = 0, stampedUnits = 0; for (std::uint32_t unit = 0; unit < trackedLayers; ++unit) { @@ -1763,7 +1766,7 @@ bool StorageTexture::Refresh() { stored[layer] = true; anyStored = true; } - if (anyStored && !keysChanged && descriptor.dccAddress != 0 && IsDccClear(uploadedKeys)) stored.assign(trackedLayers, true); + if (anyStored && !keysChanged && descriptor.dccAddress != 0 && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle)) stored.assign(trackedLayers, true); if (pendingResults) { static std::atomic reports{0}; if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] storage image 0x%llx: guest memory changed while GPU results were pending; keeping the CPU's blocks\n", static_cast(descriptor.baseAddress)); @@ -1883,18 +1886,28 @@ void traceKeyStore(const char* path, const GuestTextureResource& descriptor, std } void StorageTexture::captureGuestBytes(const std::vector* layers) { + original.resize(static_cast(guestBytes)); comparedGuestBytes = {}; - const bool complete = layers == nullptr || std::all_of(layers->begin(), layers->end(), [](bool selected) { return selected; }); + std::vector selected(trackedLayers, true); + if (layers != nullptr) selected = *layers; + for (const auto& [begin, end] : unitRuns(selected)) GuestMemory::FlushGpuWrites(descriptor.baseAddress + begin, static_cast(end - begin)); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers != nullptr && !(*layers)[layer]) continue; + if (!selected[layer]) continue; const auto offset = static_cast(layerBegin(layer) - descriptor.baseAddress); - GuestMemory::ReadCommitted(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); + GuestMemory::ReadCommittedUnsynced(layerBegin(layer), std::span(original).subspan(offset, static_cast(layerBytes(layer)))); + layerOriginal[layer] = true; } - originalValid = originalValid || complete; +} + +bool StorageTexture::originalHolds(std::uint64_t address, std::size_t bytes) const { + for (auto layer = (address - descriptor.baseAddress) / trackedLayerBytes; layer < trackedLayers && layer * trackedLayerBytes < address - descriptor.baseAddress + bytes; ++layer) { + if (!layerOriginal[static_cast(layer)]) return false; + } + return true; } bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, std::span changed, bool memoize) const { - if (!originalValid) return false; + if (!originalHolds(address, bytes)) return false; struct Exempt { const StorageTexture* previous; ~Exempt() { refreshing = previous; } @@ -1903,7 +1916,7 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, Require(address >= descriptor.baseAddress && bytes <= guestBytes && address - descriptor.baseAddress <= guestBytes - bytes, "texture comparison exceeds its snapshot"); Require(original.size() == guestBytes, "texture comparison has no complete snapshot"); GuestMemory::FlushGpuWrites(address, bytes); - if (!originalValid) return false; + if (!originalHolds(address, bytes)) return false; constexpr std::uint64_t blockBytes = 65536; const auto end = address + bytes; const auto first = address / blockBytes; @@ -1934,12 +1947,13 @@ bool StorageTexture::compareUntracked(std::uint64_t address, std::size_t bytes, constexpr std::uint64_t GenerationBaselineBytes = 1ull << 20; 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 bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + CompletePendingCpuWrites(context, descriptor.baseAddress, static_cast(guestBytes)); + if (descriptor.dccAddress != 0) CompletePendingCpuWrites(context, descriptor.dccAddress, DccKeyBytes(guestBytes)); + 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; @@ -1968,10 +1982,8 @@ void StorageTexture::upload(const std::vector* layers) { // The clear is recorded into the open batch like a direct upload (the image kept by it): a // batch of its own submitted the recorder's work first and waited for all of it, 25-40 ms // under the GPU mutex at the movie stage. APS5_NO_RECORDED_CLEAR=1 waits as before. - // Watched memory needs no copy unless an import window can stamp it: Refresh then compares - // the stamped units with the copy (WatchedImportsStampPages). static_cast(HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))); - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes)) && !WatchedImportsStampPages(context)) originalValid = false; + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); else captureGuestBytes(nullptr); forgetBorrowed(0, trackedLayers); stampLayers(false); @@ -1996,7 +2008,7 @@ void StorageTexture::upload(const std::vector* layers) { ++Profile().storageWaitedClears; } VkImageMemoryBarrier toClear{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toClear.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + toClear.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; toClear.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; toClear.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; toClear.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; @@ -2008,7 +2020,7 @@ void StorageTexture::upload(const std::vector* layers) { context.Function("vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, 1, &toClear.subresourceRange); VkImageMemoryBarrier toGeneral = toClear; toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT; toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); @@ -2020,7 +2032,7 @@ void StorageTexture::upload(const std::vector* layers) { return; } if (const auto* import = uploadedKeys == DccKeys::Uncompressed ? HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes)) : nullptr) { - if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) originalValid = false; + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); else captureGuestBytes(layers); stampLayers(true); // A whole-surface upload of a block-unit image goes through the windows too when a unit @@ -2142,9 +2154,10 @@ void StorageTexture::upload(const std::vector* layers) { if (profile) LookupOutcomes::Add(LookupOutcomes::UploadDirect, start); return; } - originalValid = true; + 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); @@ -2787,7 +2800,7 @@ bool StorageTexture::keysFillMatches(std::uint64_t address, std::size_t bytes, b } void StorageTexture::MarkDirty() { - if (descriptor.dccAddress != 0 && IsDccClear(uploadedKeys) && !IsDccClear(filledKeys)) { + if (descriptor.dccAddress != 0 && (IsDccClear(uploadedKeys) || uploadedKeys == DccKeys::ClearSingle) && !IsDccClear(filledKeys)) { traceKeyStore("first write", descriptor, guestBytes); MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; @@ -3222,9 +3235,10 @@ bool StorageTexture::evictStale(std::uint64_t address, std::size_t bytes, std::s // As the empty write-back leaves the layers: no results pending, the generation unchanged (the // stamps make their next Refresh re-upload), `original` no longer vouching for the bytes. for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (selected(layer)) layerPending[layer] = false; + if (!selected(layer)) continue; + layerPending[layer] = false; + layerOriginal[layer] = false; } - originalValid = false; if (!anyLayerPending()) { ++dropped; staleEvicted.fetch_add(1, std::memory_order_relaxed); @@ -3284,7 +3298,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz // touched the bytes a CPU-path upload copied). texture->dirty = false; texture->layerPending.assign(texture->trackedLayers, false); - texture->originalValid = false; + texture->layerOriginal.assign(texture->trackedLayers, false); it = pending.textures.erase(it); ++discarded; } else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) { @@ -3296,13 +3310,11 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz const auto unitBegin = texture->layerBegin(unit); if (!texture->layerPending[unit] || unitBegin < address || unitBegin + texture->layerBytes(unit) > end) continue; texture->layerPending[unit] = false; + texture->layerOriginal[unit] = false; unitsDropped.fetch_add(1, std::memory_order_relaxed); dropped = true; } - if (dropped) { - texture->originalValid = false; - ++discarded; - } + if (dropped) ++discarded; if (dropped && !texture->anyLayerPending()) { texture->dirty = false; it = pending.textures.erase(it); @@ -3500,7 +3512,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); recorder->EndGpuTiming(timing, guestBytes); - originalValid = false; + layerOriginal.assign(trackedLayers, false); uploadedKeys = keys; filledKeys = DccKeys::Uncompressed; keyProof = {}; @@ -3593,7 +3605,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->originalValid = false; + texture->layerOriginal.assign(texture->trackedLayers, false); if (texture->descriptor.dccAddress != 0) aliasedKeys = true; } } @@ -3642,13 +3654,19 @@ bool StorageTexture::FillClear(std::span pattern, std::u // memo hit of this submission's epoch would not consume pages the game dirtied since that // walk, the write-back's own walk would stamp them above this generation and keep their // blocks, and the fill's pattern would be lost in them. - originalValid = false; + const bool watched = GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes)); + if (watched) layerOriginal.assign(trackedLayers, false); uploadedKeys = DccKeys::Uncompressed; if (layer != WholeImage && trackedLayers > 1) { // The cleared layer's units alone: their stamps above lie below this generation, the other // units keep theirs (their pages are consumed by their own collects). const auto first = blockUnits ? static_cast((begin - descriptor.baseAddress) / trackedLayerBytes) : layer; const auto count = blockUnits ? static_cast(bytes / trackedLayerBytes) : 1u; + if (!watched) { + std::vector filled(trackedLayers, false); + std::fill_n(filled.begin() + first, count, true); + captureGuestBytes(&filled); + } const auto now = GuestMemory::CollectWritesUncached(begin, bytes); for (auto unit = first; unit < first + count; ++unit) layerGeneration[unit] = now; refreshGeneration(); @@ -3656,6 +3674,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u markLayersPending(first, count); return true; } + if (!watched) captureGuestBytes(nullptr); layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); refreshGeneration(); forgetBorrowed(0, trackedLayers); @@ -3720,7 +3739,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->originalValid = false; + if (texture != this && !texture->released && texture->overlaps(descriptor.baseAddress, static_cast(guestBytes))) texture->layerOriginal.assign(texture->trackedLayers, false); } } GuestMemory::MarkWritten(descriptor.baseAddress, static_cast(guestBytes)); @@ -3771,7 +3790,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). - originalValid = false; + if (GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) layerOriginal.assign(trackedLayers, false); + else captureGuestBytes(nullptr); uploadedKeys = DccKeys::Uncompressed; layerGeneration.assign(trackedLayers, GuestMemory::CollectWritesUncached(descriptor.baseAddress, static_cast(guestBytes))); refreshGeneration(); @@ -3998,7 +4018,7 @@ bool StorageTexture::guestBytesSettled() const { } bool StorageTexture::unchangedSinceBaseline(std::uint64_t from, std::uint64_t to) const { - const auto& baseline = originalValid ? original : generationBaseline; + const auto& baseline = originalHolds(from, static_cast(to - from)) ? original : generationBaseline; if (baseline.size() != guestBytes) return false; const auto offset = static_cast(from - descriptor.baseAddress); return GuestMemory::EqualsCommittedUnsynced(from, std::span(baseline).subspan(offset, static_cast(to - from))); @@ -4051,12 +4071,29 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { } std::vector changedBlocks(spanBlocks); const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast(lastStored - firstStored), generations, changedBlocks); - const bool compared = !tracked && compareUntracked(firstStored, static_cast(lastStored - firstStored), changedBlocks); + std::vector compared(trackedLayers, false); + if (!tracked) { + std::vector saved(trackedLayers, false); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) saved[layer] = layers[layer] && layerOriginal[layer]; + for (const auto& [begin, end] : unitRuns(saved)) { + const auto from = descriptor.baseAddress + begin; + const auto last = descriptor.baseAddress + end - 1; + if (!compareUntracked(from, static_cast(end - begin), std::span(changedBlocks).subspan(static_cast((from - spanBegin) / block), static_cast(last / block - from / block + 1)))) continue; + for (auto layer = static_cast(begin / trackedLayerBytes); layer < trackedLayers && layerBegin(layer) <= last; ++layer) compared[layer] = true; + } + } + const auto forgetStored = [&] { + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer]) layerOriginal[layer] = false; + } + }; + bool unsavedOutsideWatch = false; for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (!layers[layer]) continue; const auto begin = layerBegin(layer); const auto end = begin + layerBytes(layer); - if (!compared && (!tracked || layerGeneration[layer] == 0)) { + if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) { + unsavedOutsideWatch = unsavedOutsideWatch || !tracked; keep.emplace_back(begin, end); continue; } @@ -4065,7 +4102,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 || !edge || GuestMemory::StoredOver(from, static_cast(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to)) { + if (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; @@ -4117,18 +4154,19 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // store, the keys stay as they are, and the image is stale there (re-uploaded at its next // use, never from a matching `original`). emptyWriteBacks.fetch_add(1, std::memory_order_relaxed); - originalValid = false; + forgetStored(); settle(true); return; } if (const auto* import = HostImportFor(context, descriptor.baseAddress, static_cast(guestBytes))) { + Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); if (blockUnits) { // The kept blocks alone pass through the retiler (windows of their slices). shadowImport = import; const auto storedBytes = writeBackWindows(*import, keep, firstStored, lastStored, shadowed, imported); countStorageWriteBack(storedBytes, true); if (profile) Profile().storageGpu += timer.lap(); - originalValid = false; + forgetStored(); traceKeyStore("block write-back", descriptor, guestBytes); if (!IsDccClear(filledKeys) && uploadedKeys != DccKeys::Uncompressed) MarkDccUncompressed(context, descriptor.dccAddress, guestBytes, DccKeyCount(descriptor, guestBytes)); uploadedKeys = DccKeys::Uncompressed; @@ -4233,7 +4271,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { countStorageWriteBack(storedBytes, true); if (profile) Profile().storageGpu += timer.lap(); // The guest bytes now differ from `original`; other caches of the range see the write. - originalValid = false; + forgetStored(); // The keys go the same way as the texels: a fill recorded behind the retile when the // metadata is host-imported (no CPU wait for the title's key-writing kernels), else a CPU // store (APS5_CPU_DCC_KEYS=1 keeps the CPU store; see DccMetadata.hpp). @@ -4257,18 +4295,21 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { static std::atomic reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); - originalValid = false; + forgetStored(); for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } 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 // the content again only where it did before, or once every unit is stored. - const bool wasValid = originalValid; - if (!wasValid) GuestMemory::ReadCommitted(descriptor.baseAddress, original); + Require(!unsavedOutsideWatch, "storing storage image results pending over memory outside the write watch without the guest bytes saved before them is not implemented"); + std::vector unsaved = layerOriginal; + unsaved.flip(); + for (const auto& [begin, end] : unitRuns(unsaved)) GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast(begin), static_cast(end - begin))); DeviceBuffer linear(context, static_cast(sliceLinearBytes * arrayLayers), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); DeviceBuffer tiled(context, original.size(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); Buffer host(context, original.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -4352,7 +4393,9 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { uploadedKeys = DccKeys::Uncompressed; // The store above is the only write to these pages, so `original` is current at a fresh // generation, unless blocks were kept for the CPU: then the image is stale there. - originalValid = !skippedAny && (wasValid || std::all_of(layers.begin(), layers.end(), [](bool selected) { return selected; })); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer]) layerOriginal[layer] = !skippedLayer[layer]; + } // As in the GPU-direct path, but the store's memcpy dirtied this surface's pages in the write // watch: a memoized collect would leave them for the next walk to stamp newer than this value // (the shared edge block included, undoing the advance), so the walk is made here and diff --git a/core/libs/prx/libSceAgcDriver/PostCompile.cmake b/core/libs/prx/libSceAgcDriver/PostCompile.cmake index 7618240141..4df2315361 100644 --- a/core/libs/prx/libSceAgcDriver/PostCompile.cmake +++ b/core/libs/prx/libSceAgcDriver/PostCompile.cmake @@ -10,25 +10,25 @@ add_custom_command( VERBATIM ) -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/BufferPool.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/TextureDetilerDescriptors.cpp Graphics/src/TextureCache.cpp) endif() endforeach() -foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests) +foreach(agcTarget IN ITEMS libSceAgcDriver agc_driver_visual_test) if(TARGET ${agcTarget}) target_sources(${agcTarget} PRIVATE Graphics/src/GpuColorTransfer.cpp ${agcColorTransferHeader}) endif() endforeach() -target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/TextureTiling.cpp Graphics/src/TextureFormat.cpp Execution/src/GuestMemory.cpp) +target_sources(agc_driver_bda_device_tests PRIVATE tests/ColorTransferTests.cpp) target_include_directories(agc_driver_bda_device_tests PRIVATE ${agcTextureDetileGeneratedRoot}) if(TARGET agc_driver_visual_test) diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 7b2d31ea60..1a2d0f5c58 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -3,11 +3,16 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int DEBUG_UNAVAILABLE = static_cast(0x8A6C1000); + +} + extern "C" { int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 1; } int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { @@ -15,29 +20,36 @@ int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { return static_cast(0x8A6C9018); } -int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(uint32_t frequency) { + (void)frequency; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverSetSubmitValidationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSetSubmitValidationMode(uint32_t mode) { + (void)mode; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverGetSubmitValidationConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void* config) { + (void)config; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverGetSubmitValidationMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverGetSubmitValidationMode(uint32_t* mode) { + (void)mode; + return DEBUG_UNAVAILABLE; } -int APS5_VABI sceAgcDriverSetSubmitValidationConfig() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void* config) { + (void)config; + return DEBUG_UNAVAILABLE; +} + +int APS5_VABI sceAgcDriverGetHsOffchipParam(uint16_t* first, uint16_t* second) { + (void)first; + (void)second; + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 392eab435f..ac1c3b11a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -175,6 +175,42 @@ void failedImportRemapTests(const Context& context, const BdaTestAccess& access) ::operator delete(block, std::align_val_t{piece}); } +void spaceImportRunTests(const Context& context) { + constexpr std::size_t piece = 65536; + void* block = ::operator new(2 * piece, std::align_val_t{piece}); + const auto first = reinterpret_cast(block); + std::memset(block, 0x3c, 2 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + 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); + }; + auto* guest = static_cast(block); + change(guest, true); + change(guest + piece, true); + for (int build = 0; build < 2; ++build) { + const auto before = AddressSpaceCounters(); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(first + piece - 256, 512); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(first + piece - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a V# across one import's ranges has no descriptor"); + const auto ranges = leased.AddressRanges(); + Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin <= first && first + 2 * piece <= range.end; }), "one import's adjacent ranges are not one BDA table entry"); + leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build == 1) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap, "a V# across one import's ranges dissolved the cached address space"); + } + change(guest, false); + change(guest + piece, false); + ::operator delete(block, std::align_val_t{piece}); +} + void importWindowTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -207,6 +243,81 @@ 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) { + GuestBufferMemory plain(importing); + plain.AddReadable(address, 32); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(address, 32, 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* 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(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}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -802,31 +913,71 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { releaseSparse(block, reserved); } +bool RunSparseBufferHoleTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t unit = 65536; + constexpr std::size_t copied = 3 * unit; + bool passed = true; + for (const bool writable : {false, true}) { + for (const std::string shape : {"interior", "shortened", "committed"}) { + const auto name = std::string(writable ? "writable " : "readable ") + shape; + try { + const bool whole = shape == "committed"; + const auto reserved = shape == "shortened" ? 4 * unit : copied; + struct Reservation { + std::size_t bytes; + std::byte* data; + ~Reservation() { releaseSparse(data, bytes); } + } reservation{reserved, reserveSparse(reserved, whole ? reserved : unit)}; + if (!whole) { +#ifdef _WIN32 + Require(VirtualAlloc(reservation.data + 2 * unit, unit, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the sparse heap tail"); +#else + Require(mprotect(reservation.data + 2 * unit, unit, PROT_READ | PROT_WRITE) == 0, "cannot commit the sparse heap tail"); +#endif + } + std::memset(reservation.data, 0x31, unit); + if (whole) std::memset(reservation.data + unit, 0x3c, unit); + std::memset(reservation.data + 2 * unit, 0x72, unit); + VkBuffer seeded = VK_NULL_HANDLE; + { + Buffer seed(context, copied, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::fill(seed.Bytes().begin(), seed.Bytes().end(), std::byte{0xa5}); + seeded = seed.Handle(); + } + const auto base = reinterpret_cast(reservation.data); + GuestBufferMemory memory(context); + if (writable) memory.AddWritable(base, reserved); + else memory.AddReadable(base, reserved); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(base, reserved, adjustment); + Require(view.buffer == seeded, "the sparse upload did not reuse the poisoned buffer"); + Require(adjustment == 0 && view.offset == 0 && view.range == copied && memory.CopiedBytes() == copied, "the sparse upload has the wrong allocated span"); + auto bytes = access.bytes(view.buffer).first(copied); + const auto equals = [](std::span part, std::byte value) { + return std::all_of(part.begin(), part.end(), [value](std::byte byte) { return byte == value; }); + }; + Require(equals(bytes.first(unit), std::byte{0x31}) && equals(bytes.subspan(2 * unit, unit), std::byte{0x72}), "committed GPU bytes changed during upload"); + const bool holeZero = equals(bytes.subspan(unit, unit), whole ? std::byte{0x3c} : std::byte{}); + bytes[8] = std::byte{0x19}; + bytes[unit + 8] = std::byte{0xd4}; + bytes[2 * unit + 8] = std::byte{0x27}; + memory.WriteBack(); + Require(reservation.data[8] == (writable ? std::byte{0x19} : std::byte{0x31}) && reservation.data[2 * unit + 8] == (writable ? std::byte{0x27} : std::byte{0x72}), "sparse write-back did not preserve its writable ranges"); + if (whole) Require(reservation.data[unit + 8] == (writable ? std::byte{0xd4} : std::byte{0x3c}), "committed middle write-back changed permissions"); + Require(holeZero, "uncommitted GPU bytes retain prior buffer contents"); + std::cout << "sparse buffer holes: " << name << " passed\n"; + } catch (const std::exception& error) { + std::cerr << "sparse buffer holes: " << name << ": " << error.what() << '\n'; + passed = false; + } + } + } + return passed; +} + void hostWriteWatchExclusionTests() { #ifdef _WIN32 - if (AgcDriver::GuestMemory::WriteWatched()) { - std::uintptr_t arena = 0; - std::size_t arenaBytes = 0; - GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); - bool found = false; - for (auto cursor = arena; cursor < arena + arenaBytes;) { - MEMORY_BASIC_INFORMATION info{}; - Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) { - Require(!AgcDriver::GuestMemory::Watched(cursor, 1), "an arena host hole was reported as write-watched"); - Require(AgcDriver::GuestMemory::CollectWritesUncached(cursor, 1) == 0, "an arena host hole returned a write generation"); - Require(!AgcDriver::GuestMemory::UnchangedSince(cursor, 1, 1), "an arena host hole passed an unchanged proof"); - const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{cursor, 1, 1}}; - Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "an arena host hole passed a bulk unchanged proof"); - found = true; - break; - } - const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; - Require(next > cursor, "arena enumeration did not advance"); - cursor = next; - } - Require(found, "write-watched arena contained no host exclusion to test"); - } std::vector host(64, 0x31); const auto address = reinterpret_cast(host.data()); Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched"); @@ -1162,7 +1313,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importCrossingTests(context, access); importSpanTests(context); failedImportRemapTests(context, access); + spaceImportRunTests(context); importWindowTests(context); + importJoinTests(context); heapMirrorTests(context, access); importedHeapMirrorTests(context, access); importWatchTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 0d90f1c583..b8c859c022 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -15,6 +15,7 @@ struct BdaTestAccess { }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); +bool RunSparseBufferHoleTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); void RunGuestAllocationTests(); void RunGuestLeaseWaitTests(); void RunUnmappedGapTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp index ffb977d6ea..dfadd4aabe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/CmaskFastClear.cpp @@ -145,22 +145,14 @@ void run(AgcDriver::VulkanDevice& device, ColorTileMode mode, const std::string& expect(device, color, [](std::uint32_t x, std::uint32_t y) { return y < 4 ? Drawn : y >= 8 && x >= 24 ? Clear : Stale; }, name + " fast-cleared bottom edge tiles under a draw of the top rows"); } -void invalidCode(AgcDriver::VulkanDevice& device) { +void unknownCode(AgcDriver::VulkanDevice& device) { const auto color = target(ColorTileMode::RenderTarget); prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3); eliminate(device, color); - { - std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); - device.WaitIdle(); - } + expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a tile with CMASK code 3 eliminated"); prepare(color, [](std::uint32_t, std::uint32_t) { return true; }); - try { - eliminate(device, color); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()).find("were recorded, which are not a single-sample fast-clear code") != std::string::npos, std::string("unexpected failure: ") + error.what()); - return; - } - throw std::runtime_error("a CMASK code 3 read by the GPU pass did not throw"); + eliminate(device, color); + expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3"); } } @@ -182,7 +174,7 @@ int main() { } run(*device, ColorTileMode::Linear, "linear"); run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X"); - invalidCode(*device); + unknownCode(*device); device->WaitIdle(); device.reset(); { diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 0aebfe320e..40786cc1e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -60,6 +60,7 @@ void RunColorTargetLayoutTests() { Require(DecodeColorTileMode(0x4dc14000) == ColorTileMode::Standard4KB, "4 KiB standard color descriptor was rejected"); const ColorTargetLayout standard(256, 256, ColorTileMode::Standard4KB); Require(standard.Bytes() == ComputeSurfaceSize(ComputeElementMipLayout(TextureTileMode::kStandard4KB, 4, 256, 256, 1), 1) && standard.Alignment() == 4096, "4 KiB standard color layout differs from the texture layout"); + Require(ColorTargetLayout(1920, 1080, ColorTileMode::RenderTarget, 16).Bytes() == 0x1fe0000u, "four-sample 1920x1080 color target should fit before the next 32 MiB allocation"); Require(standard.Offset(32, 0) == 4096 && standard.Offset(0, 32) == 8 * 4096 && standard.Offset(1, 0) == 4 && standard.Offset(0, 1) == 16, "4 KiB standard block or element addressing is incorrect"); std::vector standardSeen(1024); for (std::uint32_t y = 0; y < 32; ++y) { @@ -207,4 +208,55 @@ void RunColorTargetLayoutTests() { Require(back == source, "4 KiB XOR color tiling round trip lost pixels"); reject([mode] { static_cast(ColorTargetLayout(40, 20, mode, 4, 0x1000)); }); } + + Require(DecodeColorTileMode(0x0dc68000u) == ColorTileMode::D64KBX, "the observed SW_64KB_D_X color descriptor was rejected"); + 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); }); + reject([] { DecodeColorTileMode((0x0dc68000u & ~(0x1fu << 19u)) | (1u << 19u)); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x80); }); + reject([] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, 0x10000); }); + for (const std::uint32_t columnBits : {0x1000u, 0x2000u, 0x3000u}) reject([columnBits] { ColorTargetLayout(64, 64, ColorTileMode::D64KBX, 4, columnBits); }); + const std::array, 5> sourceOffsets{{{27647u, 66048u}, {29694u, 212992u}, {62204u, 344064u}, {45304u, 540672u}, {110832u, 933888u}}}; + for (const std::uint32_t bpe : {1u, 2u, 4u, 8u, 16u}) { + const ColorTargetLayout sourceLayout(257, 129, ColorTileMode::D64KBX, bpe, 0x4000); + const auto& offsets = sourceOffsets[std::countr_zero(bpe)]; + for (std::uint32_t xorTag = 0; xorTag < 256u; ++xorTag) { + if ((xorTag & ~0xcfu) != 0) continue; + const ColorTargetLayout xorLayout(1, 1, ColorTileMode::D64KBX, bpe, xorTag << 8u); + Require(xorLayout.Offset(0, 0) == (xorTag << 8u), "a display XOR color origin differs from its pipe/bank bits"); + } + Require(sourceLayout.Offset(127, 63) == offsets[0] && sourceLayout.Offset(256, 128) == offsets[1], "display XOR color offsets differ from the AMD GFX10 non-RB+ source reference"); + 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}) { + 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); + Require(layout.Alignment() == block[0] && layout.BlocksPerRow() == 3u && layout.Bytes() == ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(mode), bpe, width, height, 1), 1), "a 64 KiB color layout differs from its texture geometry"); + for (const auto x : {0u, 1u, block[1] - 1u, block[1], block[1] + 1u, width - 1u}) { + for (const auto y : {0u, 1u, block[2] - 1u, block[2], block[2] + 1u, height - 1u}) { + std::size_t expected = 0; + for (std::uint32_t bit = 0; bit < 16u; ++bit) expected |= static_cast(std::popcount((x & equation->bits[bit] & 0xfffu) ^ ((y << 12u) & equation->bits[bit] & 0xfff000u)) & 1) << bit; + const auto blockIndex = (y / block[2]) * 3u + x / block[1]; + Require(layout.Offset(x, y) == blockIndex * 65536u + (expected ^ 0x4600u), "a 64 KiB color offset differs from its mode's equation across block boundaries"); + } + } + std::vector source(layout.LinearBytes()); + for (std::size_t index = 0; index < source.size(); ++index) source[index] = static_cast((index * 31u + index / 251u) & 255u); + std::vector tiled(layout.Bytes()); + std::vector restored(source.size()); + layout.Tile(source, tiled); + layout.Detile(tiled, restored); + Require(restored == source, "64 KiB display/render-target XOR color tiling lost pixels"); + } + } + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); + target = {reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::D64KBX}; + target.pipeBankXor = 0x0200; + WriteColorTarget(target, pixels); + readback.fill(std::byte{0}); + ReadColorTarget(target, readback); + Require(readback == pixels && guest[0] == std::byte{0x6b}, "display XOR guest color transfer lost pixels or wrote the untagged block origin"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index bf83e852c7..ff2d18e119 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include namespace { @@ -19,6 +21,7 @@ constexpr std::uint32_t FormatR32Uint = 20; constexpr std::uint32_t FormatR32Float = 22; constexpr std::uint32_t FormatR11G11B10Float = 36; constexpr std::uint32_t FormatR8G8B8A8Unorm = 56; +constexpr std::uint32_t TileStandard64KB = 0x9; constexpr std::uint32_t TileDepth64KB = 0x18; constexpr std::uint32_t TileRenderTarget64KB = 0x1b; constexpr std::uint32_t Type2D = 9; @@ -31,7 +34,10 @@ THandle newHandle() { return reinterpret_cast(nextHandle += 0x10); } -VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo*, const VkAllocationCallbacks*, VkImage* image) { +VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; + +VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { + lastImageFormat = info->format; *image = newHandle(); return VK_SUCCESS; } @@ -97,6 +103,16 @@ VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormat properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; } +VKAPI_ATTR void VKAPI_CALL formatPropertiesWithoutD16S8(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format != VK_FORMAT_D16_UNORM_S8_UINT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; +} + +VKAPI_ATTR void VKAPI_CALL formatPropertiesOnlyD32S8(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_D32_SFLOAT_S8_UINT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; +} + PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { static const std::map table{ {"vkCreateImage", reinterpret_cast(createImage)}, @@ -151,6 +167,57 @@ std::shared_ptr lookup(const Context& context, const View& view) { return DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); } +void TestD16StencilFallback(const Context& context) { + Require(HostDepthFormat(context, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D16_UNORM_S8_UINT && HostDepthBiasConstant(context, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f) == 2.0f, "a device with D16_UNORM_S8_UINT must keep it and its depth bias"); + static int withoutTag = 0; + Context without = context; + without.physical = reinterpret_cast(&withoutTag); + without.formatProperties = formatPropertiesWithoutD16S8; + constexpr std::uint64_t d16s8 = 0x70000000; + constexpr std::uint64_t d16s8Stencil = 0x70800000; + Require(HostDepthFormat(without, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D24_UNORM_S8_UINT, "a device without D16_UNORM_S8_UINT must store it as D24_UNORM_S8_UINT"); + Require(HostDepthFormat(without, VK_FORMAT_D32_SFLOAT_S8_UINT) == VK_FORMAT_D32_SFLOAT_S8_UINT && HostDepthFormat(without, VK_FORMAT_D16_UNORM) == VK_FORMAT_D16_UNORM, "only D16_UNORM_S8_UINT is replaced"); + DepthSurfaceView(without, {d16s8, d16s8Stencil, {256, 256}, VK_FORMAT_D16_UNORM_S8_UINT, 1.0f, 0}); + Require(lastImageFormat == VK_FORMAT_D24_UNORM_S8_UINT, "a D16_UNORM_S8_UINT surface was not created as D24_UNORM_S8_UINT on a device without it"); + Require(lookup(without, {d16s8, FormatR16Unorm, 256, 256}) != nullptr, "an R16 view of a D16 surface stored as D24 must still sample its depth plane"); + Require(lookup(without, {d16s8Stencil, FormatR8Uint, 256, 256}) != nullptr, "an 8-bit view of the stencil plane of a D16 surface stored as D24 must sample it"); + Require(HostDepthBiasConstant(without, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f) == 512.0f, "a 16-bit depth bias must be scaled by 256 for D24 storage"); + Require(HostDepthBiasConstant(without, VK_FORMAT_D32_SFLOAT_S8_UINT, 2.0f) == 2.0f, "a D32 depth bias must not be scaled"); + const auto refused = [&](const View& view) { + try { + lookup(without, view); + } catch (const std::runtime_error& error) { + return std::string_view(error.what()).find("a depth plane that the device stores in another depth format is not implemented") != std::string_view::npos; + } + return false; + }; + Require(refused({d16s8, FormatR16Unorm, 256, 256, TileDepth64KB, Type2DArray, 1}), "two slices of a D16 surface stored as D24 must be refused, as the plane copy would read the wider image as 16-bit texels"); + Require(refused({d16s8, FormatR16Uint, 256, 256, TileStandard64KB}), "an R16_UINT view of a D16 surface stored as D24 must be refused"); + std::string storageError; + try { + const auto words = tsharp({d16s8, FormatR16Unorm, 256, 256}); + DepthSurfaceStorage(without, words, DecodeTextureResource(words), 0); + } catch (const std::runtime_error& error) { + storageError = error.what(); + } + Require(storageError.find("depth plane") != std::string::npos && storageError.find("stored as") != std::string::npos, "storage access to the depth plane of a D16 surface stored as D24 must throw"); + ClearDepthSurfaces(without.device); + + static int onlyTag = 0; + Context only = context; + only.physical = reinterpret_cast(&onlyTag); + only.formatProperties = formatPropertiesOnlyD32S8; + Require(HostDepthFormat(only, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D32_SFLOAT_S8_UINT, "a device with neither D16 nor D24 stencil formats must store D16_UNORM_S8_UINT as D32_SFLOAT_S8_UINT"); + Require(HostDepthBiasConstant(only, VK_FORMAT_D16_UNORM_S8_UINT, 0.0f) == 0.0f, "a zero depth bias needs no scaling"); + bool threw = false; + try { + HostDepthBiasConstant(only, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "a constant 16-bit depth bias on D32_SFLOAT_S8_UINT storage must throw"); +} + } void RunDepthSurfaceReuseTests() { @@ -163,6 +230,8 @@ void RunDepthSurfaceReuseTests() { context.memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; context.limits.maxFramebufferWidth = 16384; context.limits.maxFramebufferHeight = 16384; + context.limits.maxFramebufferLayers = 1; + TestD16StencilFallback(context); constexpr std::uint64_t depth32 = 0x40000000; constexpr std::uint64_t depth16 = 0x50000000; constexpr std::uint64_t depthStencil = 0x60000000; diff --git a/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp b/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp new file mode 100644 index 0000000000..d385028db2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ExecutionTestNotBuilt.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::puts("skipped, this build leaves the test out with ANYPS5_AGC_EXECUTION_TESTS"); + return 77; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferByteStores.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferByteStores.cpp new file mode 100644 index 0000000000..0383aa0bac --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferByteStores.cpp @@ -0,0 +1,119 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target(bool byteAccess) { + static constexpr std::array withBytes{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + }; + static constexpr std::array withoutBytes{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, byteAccess ? std::span(withBytes) : std::span(withoutBytes), extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t compareExchanges = 0; + std::size_t byteStores = 0; + std::size_t byteTypes = 0; + bool byteCapability = false; + bool byteBuffers = false; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "formatted buffer byte stores: missing SPIR-V module"); + Counts counts; + std::uint32_t byteType = 0; + std::vector bytePointers; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "formatted buffer byte stores: malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges; + if (opcode == spv::OpCapability && words[offset + 1] == spv::CapabilityStorageBuffer8BitAccess) counts.byteCapability = true; + if (opcode == spv::OpTypeInt && words[offset + 2] == 8u) { + ++counts.byteTypes; + byteType = words[offset + 1]; + } + if (opcode == spv::OpTypePointer && words[offset + 2] == spv::StorageClassStorageBuffer && words[offset + 3] == byteType && byteType != 0) bytePointers.push_back(words[offset + 1]); + if (opcode == spv::OpName) { + const auto name = std::string_view(reinterpret_cast(&words[offset + 2])); + if (name == "buffers_u8") counts.byteBuffers = true; + } + offset += count; + } + std::vector byteChains; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpAccessChain) { + for (const auto pointer : bytePointers) { + if (words[offset + 1] == pointer) byteChains.push_back(words[offset + 2]); + } + } + if (opcode == spv::OpStore) { + for (const auto chain : byteChains) { + if (words[offset + 1] == chain) ++counts.byteStores; + } + } + offset += count; + } + return counts; +} + +Counts Compile(bool byteAccess, bool vertex = false) { + std::vector code{0xe0000000u, 0x80020a00u, 0xbf8c3f70u, 0xe0100000u, 0x80020a00u}; + if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; + RecompileRequest request{}; + request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(byteAccess); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + if (vertex) request.context.vertex = ShaderVertexStageInfo{}; + else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto bytes = Compile(true); + Check(bytes.compareExchanges == 0, "formatted buffer byte stores: a store still patches its dword with " + std::to_string(bytes.compareExchanges) + " compare-exchange loops"); + Check(bytes.byteBuffers && bytes.byteCapability && bytes.byteTypes == 1, "formatted buffer byte stores: the byte view of the buffers is not declared under StorageBuffer8BitAccess"); + Check(bytes.byteStores >= 4, "formatted buffer byte stores: the 8-bit components are stored with " + std::to_string(bytes.byteStores) + " byte stores, expected at least 4"); + const auto dwords = Compile(false); + Check(dwords.compareExchanges > 0 && !dwords.byteBuffers && !dwords.byteCapability, "formatted buffer byte stores: without StorageBuffer8BitAccess the stores do not fall back to the dword patch"); + std::printf("formatted buffer byte stores tests passed (%zu byte stores, %zu compare-exchange loops without the capability)\n", bytes.byteStores, dwords.compareExchanges); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp index 24ed571cd9..5cd2842612 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FormattedBufferOnce.cpp @@ -104,7 +104,7 @@ int main() { const auto storeThree = Compile(3, true); Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each"); Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only"); - Check(three.words - one.words < one.words / 10u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words to a " + std::to_string(one.words) + "-word module"); + Check(three.words - one.words < one.words - none.words, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared function (" + std::to_string(one.words - none.words) + ")"); const auto gpuThree = Compile(3, false, true); Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one"); Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9357960661..3092e67152 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -20,6 +20,7 @@ #include "CacheKey.hpp" #include "Optimization/ResourceProgram.hpp" #include "BdaAbi.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -50,6 +51,7 @@ using AgcDriver::Graphics::Require; alignas(256) std::array colorMemory{}; alignas(256) std::array sliceMemory{}; +alignas(256) std::array volumeKeyMemory{}; AgcDriver::QueueState makeState() { AgcDriver::QueueState queue; @@ -450,6 +452,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; @@ -597,17 +651,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, "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, "only triangular"}, std::pair{0x40069u, "only triangular"}, std::pair{0x4006au, "only triangular"}}) { + 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"}}) { queue.context[0x2db] = parameters; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } - queue.context[0x2db] = 0x40049; - for (const auto [maximum, minimum] : {std::pair{0x41100000u, 0x3f800000u}, std::pair{0x42800000u, 0x40000000u}}) { + queue.context[0x2d6] = 0x1043f; + queue.context[0x2db] = 0x4006a; + queue.context[0x286] = 0x41100000; + 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"); + 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"}}) { queue.context[0x286] = maximum; queue.context[0x287] = minimum; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "tessellation level clamps"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } + queue.context[0x2d6] = 0xc355; + queue.context[0x2db] = 0x40049; queue.context[0x286] = 0x42800000; queue.context[0x287] = 0x3f800000; queue.context[0x2d5] = 0x202d; @@ -615,6 +678,8 @@ void ShaderStageTests() { queue.context[0x2d5] = 0x0200210d; queue.context[0x2d6] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "control-point counts"); + queue.context[0x2d6] = 0xc300; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NUM_PATCHES is zero"); queue = makeState(); for (const auto value : {0x2003u, 0x2018u, 0x20c0u, 0x80002000u}) { queue.context[0x2d5] = value; @@ -814,6 +879,30 @@ void ColorPipeBankXorTests() { const auto color = AgcDriver::Graphics::DecodeState(queue).color; Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::D4KBX && color.address == block + 0x3000u && color.pipeBankXor == pipeBankXor, "a SW_4KB_D_X color base did not split into its 4 KiB block base and pipe/bank XOR " + std::to_string(pipeBankXor)); } + queue.context[0x3b8] = 0x0dc68000u; + for (const std::uint32_t pipeBankXor : {0u, 0x4600u, 0xcf00u}) { + 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::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"); + } + 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); + expectFailure([&queue] { AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); }, "unsupported display color pipe/bank XOR column bits"); + } + static std::vector mipStorage(16u * blockBytes); + const auto mipBase = (reinterpret_cast(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + const auto mips = AgcDriver::Graphics::ComputeElementMipLayout(AgcDriver::Graphics::TextureTileMode::kD64KBX, 4, 256, 128, 5); + const auto sliceBytes = AgcDriver::Graphics::ComputeSurfaceSize(mips, 1); + queue.context[0x318] = static_cast((mipBase + 0x0200u) >> 8u); + queue.context[0x390] = static_cast((mipBase + 0x0200u) >> 40u); + queue.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + queue.context[0x3b8] |= 1u; + queue.context[0x31b] = (2u << 26u) | 1u | (1u << 13u); + const auto mipColor = AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0); + Require(mipColor.mip == 2 && mipColor.mipCount == 5 && mipColor.extent.width == 64 && mipColor.extent.height == 32 && mipColor.surfaceAddress == mipBase + sliceBytes && mipColor.address == mipBase + sliceBytes + mips[2].tiledOffset && mipColor.mipTail == mips[2].tail && mipColor.pipeBankXor == 0x0200u, "a display XOR color view lost its array slice, mip placement or XOR"); } void HiddenUserWordVertexTablesTests() { @@ -1300,8 +1389,19 @@ void DepthStencilTests() { queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); + queue.context[0x31b] = 0; + const auto plain = AgcDriver::Graphics::DecodeState(queue); + queue.context[0x31b] = 35u << 13u; + const auto plainRange = AgcDriver::Graphics::DecodeState(queue); + Require(plainRange.color.address == plain.color.address && plainRange.color.bytes == plain.color.bytes && plainRange.color.arrayLastSlice == plain.color.arrayLastSlice, "a color view of slices 0 to 35 of a target with no array slices did not render into its only slice"); + queue.context[0x31b] = 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice"); queue.context[0x31b] = 1u << 13u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "several array slices"); + const auto firstSlice = AgcDriver::Graphics::DecodeState(queue); + Require(firstSlice.color.address == reinterpret_cast(colorMemory.data()), "a color view of several slices did not retain its first slice"); + queue.context[0x207] = 1u << 18u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + queue.context[0x207] = 0; const auto sliced = reinterpret_cast(sliceMemory.data()); queue.context[0x318] = static_cast(sliced >> 8u); queue.context[0x390] = static_cast(sliced >> 40u); @@ -1311,17 +1411,38 @@ void DepthStencilTests() { queue.context[0x3b8] = 0x09000001; const auto arraySlice = AgcDriver::Graphics::DecodeState(queue); Require(arraySlice.color.address == sliced + 1024u && arraySlice.color.bytes == 1024u && arraySlice.color.depth == 1u, "a color view of one slice of a 2D array target did not move the target by one slice"); + queue.context[0x31b] = 1u << 13u; + const auto range = AgcDriver::Graphics::DecodeState(queue); + Require(range.color.address == sliced && range.color.bytes == 1024u, "a color view of slices 0 to 1 of a 2D array target did not render into its first slice"); + queue.context[0x207] = 1u << 18u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "layered draws into a color view of several array slices"); + queue.context[0x207] = 0; queue.context[0x31b] = 2u | (2u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); + queue.context[0x31b] = 1u << 14u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the array surface"); queue.context[0x3b8] = 0x0a000003; queue.context[0x31b] = 2u | (2u << 13u); const auto volume = AgcDriver::Graphics::DecodeState(queue); Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice"); + Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered"); queue.context[0x31b] = 4u | (4u << 13u); - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "beyond the 3D surface"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface"); + queue.context[0x31b] = 2u | (1u << 13u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "ends before its first slice"); + queue.context[0x31b] = 1u | (3u << 13u); + 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"); + 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; queue.context[0x31b] = 0; queue.context[0x31c] |= 0x10000000; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets"); + const auto volumeKeys = reinterpret_cast(volumeKeyMemory.data()); + queue.context[0x325] = static_cast(volumeKeys >> 8u); + queue.context[0x3a8] = static_cast(volumeKeys >> 40u); + Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == volumeKeys, "a DCC 3D color target lost its keys"); } void OneDimensionalColorTests() { @@ -1349,6 +1470,58 @@ void htileDepthClearTests() { Require(address(std::span(kernel).first(16), {0, 0, 0x0dc50000u, 0x40u, 0u, 0x00000ff0u}, wave) == 0, "a truncated kernel was taken for RADV's HTILE update"); } +void memoryValueFillTests() { + static constexpr std::array kernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + const std::array wave{64, 1, 1}; + const std::array userData{0x50b00000u, 0x00040020u, 0x20000u, 0x00014204u, 0x34be64e0u, 0x00100020u, 1u, 0x0004dfacu}; + const auto match = [&](std::span code, std::array words, std::array threads, std::array groups) { + return AgcDriver::Graphics::MatchMemoryValueFill(code, words, threads, groups); + }; + const auto fill = match(kernel, userData, wave, {2048, 1, 1}); + Require(fill.has_value() && fill->address == 0x2050b00000ull && fill->bytes == 0x80000u && fill->valueAddress == 0x2034be64e0ull, "Kena's HTILE fill from a value in memory was not recognized"); + const auto partial = match(kernel, userData, wave, {1024, 1, 1}); + Require(partial.has_value() && partial->bytes == 0x40000u, "a fill of half the groups did not cover half the buffer"); + auto records = userData; + records[2] = 0x100u; + const auto clipped = match(kernel, records, wave, {2048, 1, 1}); + Require(clipped.has_value() && clipped->bytes == 0x400u, "a fill past NUM_RECORDS was not clipped to it"); + Require(!match(kernel, userData, {32, 1, 1}, {2048, 1, 1}), "the memory value fill matched with another group size"); + Require(!match(kernel, userData, wave, {2048, 2, 1}), "a two-dimensional dispatch was taken for a linear fill"); + auto strided = userData; + strided[1] = 0x00080020u; + Require(!match(kernel, strided, wave, {2048, 1, 1}), "a fill with an 8-byte stride was taken for a dword fill"); + auto indexed = userData; + indexed[3] |= 1u << 23u; + Require(!match(kernel, indexed, wave, {2048, 1, 1}), "a buffer with ADD_TID_ENABLE was taken for a plain fill"); + auto nowhere = userData; + nowhere[4] = 0; + nowhere[5] = 0; + Require(!match(kernel, nowhere, wave, {2048, 1, 1}), "a fill from a null value pointer was recognized"); + auto empty = userData; + empty[6] = 0; + Require(!match(kernel, empty, wave, {2048, 1, 1}), "a fill whose value buffer has no records was taken for a fill from memory"); + auto raw = userData; + raw[5] = 0x20u; + raw[6] = 4u; + Require(match(kernel, raw, wave, {2048, 1, 1}).has_value(), "a fill from a 4-byte raw value buffer was not recognized"); + raw[6] = 3u; + Require(!match(kernel, raw, wave, {2048, 1, 1}), "a fill from a value buffer shorter than a dword was taken for a fill from memory"); + for (const std::uint32_t format : {20u, 21u, 22u}) { + auto typed = userData; + typed[3] = (typed[3] & ~(0x7fu << 12u)) | (format << 12u); + Require(match(kernel, typed, wave, {2048, 1, 1}).has_value(), "a fill through a 32-bit buffer format was not recognized"); + } + for (const std::uint32_t format : {0x27u, 0x0du, 0x4bu}) { + auto converted = userData; + converted[3] = (converted[3] & ~(0x7fu << 12u)) | (format << 12u); + Require(!match(kernel, converted, wave, {2048, 1, 1}), "a fill through a converting buffer format was taken for a raw dword fill"); + } + auto other = kernel; + other[6] ^= 1u; + Require(!match(other, userData, wave, {2048, 1, 1}), "a different kernel was taken for the memory value fill"); + Require(!match(std::span(kernel).first(8), userData, wave, {2048, 1, 1}), "a truncated kernel was taken for the memory value fill"); +} + void depthMaintenanceTests() { for (const auto mode : {0x4u, 0x8u, 0x80u, 0x100u, 0x1000u, 0x4000u}) { for (const auto clear : {0u, 1u, 2u, 3u}) { @@ -1502,6 +1675,8 @@ void DepthBoundsBiasTests() { queue.context[0x80] = 0; queue.userConfig[0x242] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); + queue.userConfig[0x242] = 3; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "polygon mode, depth bias or nonstandard rasterization"); queue.userConfig[0x242] = 4; queue.context[0x205] = 0x00001a4au; queue.context[0x2de] = 0x1f0u; @@ -1802,8 +1977,8 @@ void cmaskTests() { cmaskMemory.fill(0xff); setTile(2, 0); setTile(3, 3); - expectFailure([&] { AgcDriver::Graphics::RunColorMetadataPass(context, *pass); }, "not a single-sample fast-clear code"); - Require(texels(0x5a5a5a5au), "a refused pass changed the texels"); + AgcDriver::Graphics::RunColorMetadataPass(context, *pass); + Require(tiles([](std::uint32_t tile) { return tile == 2; }) && cmaskIs(0xff), "a tile with a CMASK code other than 0 or 15 did not keep its texels"); queue.context[0x31c] |= 0x10000000; const auto keysAddress = reinterpret_cast(dccKeys.data()); @@ -1979,6 +2154,54 @@ void multisampleTests() { state = DecodeState(queue); Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto log2 : {0u, 1u, 2u, 3u}) { + queue = log2 == 0 ? makeState() : multisampled(); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u; + queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u; + } + std::vector sampleReads; + AgcDriver::Graphics::RegisterReadLog() = &sampleReads; + const auto coveredCenter = DecodeState(queue); + const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs"); + for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register"); + Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection); + for (const auto centroid : {4u, 64u, 68u}) { + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode"); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u | centroid; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode"); + } + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + for (const auto offset : {0x1b3u, 0x1b4u}) { + queue.context.erase(offset); + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs"); + queue.context[offset] = 2u; + } + const auto withProgram = queue; + queue.shader[0x008] = 0; + queue.context[0x8e] = 0; + queue.context.erase(0x1b3); + queue.context.erase(0x1b4); + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program"); + queue = withProgram; + queue.context[0x2f8] |= 0x10u; + expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits"); + } queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); @@ -2031,6 +2254,15 @@ void multisampleTests() { setCmaskAddress(queue, 1); auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); + const auto rasterizedResolve = queue; + queue.context[0x2f8] = 0; + pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1, "a CB resolve with a single-sample rasterizer did not take its sample count from the source target"); + queue = rasterizedResolve; + queue.context[0x2f8] = 0x00200002; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6aeu; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "differs from the rasterizer's"); + queue = rasterizedResolve; queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); queue = multisampled(); @@ -2073,6 +2305,102 @@ void multisampleTests() { MultisampledCmask unaddressed; unaddressed.NoteFill(0, 0x1000, 0); Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); + + using AgcDriver::Graphics::ColorTarget; + using AgcDriver::Graphics::ColorTileMode; + using AgcDriver::Graphics::MultisampledColorBytes; + using AgcDriver::Graphics::MultisampledColorFill; + using AgcDriver::Graphics::MultisampledFillClearColor; + ColorTarget halfFloat{}; + halfFloat.address = 0x40000000; + halfFloat.extent = {960, 540}; + halfFloat.format = VK_FORMAT_R16G16B16A16_SFLOAT; + halfFloat.tileMode = ColorTileMode::RenderTarget; + halfFloat.elementBytes = 8; + halfFloat.samples = 4; + Require(MultisampledColorBytes(halfFloat) == 0xff0000, "a 4x 960x540 8-byte target is not 64x32 pixels per 64 KiB block"); + auto srgb = halfFloat; + srgb.extent = {1536, 1536}; + srgb.format = VK_FORMAT_R8G8B8A8_SRGB; + srgb.elementBytes = 4; + Require(MultisampledColorBytes(srgb) == 0x2400000, "a 4x 1536x1536 4-byte target is not 64x64 pixels per 64 KiB block"); + auto narrow = srgb; + narrow.extent = {130, 60}; + narrow.samples = 2; + Require(MultisampledColorBytes(narrow) == 0x30000, "a 2x 4-byte target is not 64x128 pixels per 64 KiB block"); + narrow.samples = 8; + Require(MultisampledColorBytes(narrow) == 0x50000, "an 8x 4-byte target is not 32x64 pixels per 64 KiB block"); + srgb.extent = {100, 100}; + Require(MultisampledColorBytes(srgb) == 0x40000, "a 4x 100x100 4-byte target does not span 2x2 64 KiB blocks"); + srgb.tileMode = ColorTileMode::Standard4KB; + Require(MultisampledColorBytes(srgb) == 0x31000, "a 4x 4-byte target is not 16x16 pixels per 4 KiB block"); + srgb.tileMode = ColorTileMode::Linear; + Require(MultisampledColorBytes(srgb) == 0, "a linear multisampled target has a footprint"); + + MultisampledColorFill fill(halfFloat); + Require(!fill.TakeFill(), "an unfilled target cleared"); + fill.NoteFill(0x40000000, 0xff0000, 0); + const auto zero = fill.TakeFill(); + Require(zero && *zero == 0 && !fill.TakeFill(), "a fill of the whole color memory did not clear once"); + fill.NoteFill(0x40ff0000, 0x10000, 0); + fill.NoteFill(0x3fff0000, 0x10000, 0); + Require(!fill.TakeFill(), "a fill next to the color memory cleared it"); + fill.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { fill.TakeFill(); }, "partly covers"); + fill.NoteFill(0x3fff0000, 0x1010000, 0x3c003c00u); + const auto one = fill.TakeFill(); + Require(one && *one == 0x3c003c00u, "a fill over and around the color memory did not clear it"); + MultisampledColorFill linear(srgb); + linear.NoteFill(0x40001000, 0x1000, 0); + Require(!linear.TakeFill(), "a fill past the start of a target with no modeled footprint was taken"); + linear.NoteFill(0x40000000, 0x1000, 0); + expectFailure([&] { linear.TakeFill(); }, "footprint is not modeled"); + + VkClearColorValue clear{}; + Require(MultisampledFillClearColor(halfFloat, 0x3c003c00u, clear) && clear.float32[0] == 1.0f && clear.float32[3] == 1.0f, "a half-float fill of 1.0 did not clear to 1.0"); + Require(MultisampledFillClearColor(srgb, 0, clear) && clear.float32[0] == 0.0f && clear.float32[3] == 0.0f, "an sRGB fill of 0 did not clear to 0"); + Require(MultisampledFillClearColor(srgb, 0x80ffffffu, clear) && clear.float32[2] == 1.0f && clear.float32[3] == 128.0f / 255.0f, "an sRGB fill of white with linear alpha 0x80 did not clear to it"); + Require(!MultisampledFillClearColor(srgb, 0x00000080u, clear), "an sRGB fill whose color decodes inexactly cleared"); + + using AgcDriver::Graphics::TakeMultisampledClear; + halfFloat.clearWords = {0x3c003c00u, 0x3c003c00u}; + MultisampledCmask clearedCmask(0x50000000, 0x1000); + MultisampledColorFill pending(halfFloat); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a target with no CMASK clear and no fill cleared"); + pending.NoteFill(0x40000000, 0xff0000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 0.0f, "a fill of the whole color memory did not clear to its value"); + pending.NoteFill(0x40000000, 0xfe0000, 0); + expectFailure([&] { TakeMultisampledClear(clearedCmask, pending, halfFloat, clear); }, "partly covers"); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f, "a pending CMASK clear did not take precedence over a partial fill of the color memory"); + Require(!TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a partial fill of the color memory outlived the CMASK clear over it"); + pending.NoteFill(0x40000000, 0xff0000, 0); + clearedCmask.NoteFill(0x50000000, 0x1000, 0); + Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color"); +} + +void clipDistanceTests() { + auto queue = makeState(); + queue.context[0x207] = 0x0040000f; + Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == 0x0040000fu, "DecodeState dropped the PA_CL_VS_OUT_CNTL value the validator quotes"); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "four clip distances were rejected by the precheck"); + queue.context[0x207] = 0x004001ff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck"); +} + +void framebufferLayerTests() { + AgcDriver::Graphics::Context context{}; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 3; + AgcDriver::Graphics::Framebuffer accepted(context, VK_NULL_HANDLE, {}, {64, 64}, 3); + for (const VkRenderPass pass : std::array{VK_NULL_HANDLE, reinterpret_cast(1)}) { + expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 0); }, "framebuffer layers exceed device limits"); + expectFailure([&] { AgcDriver::Graphics::Framebuffer rejected(context, pass, {}, {64, 64}, 4); }, "framebuffer layers exceed device limits"); + } } void DepthClipTests() { @@ -2152,7 +2480,7 @@ void AlphaToCoverageTests() { Require(AgcDriver::Graphics::DrawRejection(queue, false).find("alpha-to-coverage") != std::string::npos, "the precheck accepted unsupported alpha-to-coverage"); } queue.context[0x2dc] = 0xaa01; - const auto exportFormat = queue.context.at(0x1c5); + const std::uint32_t exportFormat = queue.context.at(0x1c5); for (const auto format : {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"); @@ -2233,6 +2561,10 @@ struct MockVulkan { std::uint32_t boundFirst = 0; VkPipelineBindPoint boundPoint = VK_PIPELINE_BIND_POINT_MAX_ENUM; std::map pipelineLayoutPushConstantSize; + std::map> pipelineLayoutSets; + std::map pipelineLayoutFlags; + std::map> pipelineLayoutPushRanges; + std::map libraryLayouts; std::uint32_t pipelineCreateCount = 0; std::vector> pipelineSpecializations; VkPipeline boundPipeline = VK_NULL_HANDLE; @@ -2391,6 +2723,19 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo* info, const VkAllocationCallbacks*, VkPipelineLayout* layout) { *layout = makeHandle(); mock.pipelineLayoutPushConstantSize[*layout] = info->pushConstantRangeCount > 0 ? info->pPushConstantRanges[0].size : 0; + if (info->setLayoutCount != 0) mock.pipelineLayoutSets[*layout].assign(info->pSetLayouts, info->pSetLayouts + info->setLayoutCount); + mock.pipelineLayoutFlags[*layout] = info->flags; + if (info->pushConstantRangeCount != 0) mock.pipelineLayoutPushRanges[*layout].assign(info->pPushConstantRanges, info->pPushConstantRanges + info->pushConstantRangeCount); + ++mock.live; + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + Require(count == 1 && (infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0, "mock expects a graphics pipeline library"); + const auto* library = static_cast(infos[0].pNext); + Require(library != nullptr && library->sType == VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, "missing graphics pipeline library description"); + mock.libraryLayouts[library->flags] = infos[0].layout; + *pipelines = makeHandle(); ++mock.live; return VK_SUCCESS; } @@ -2405,8 +2750,8 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateShaderModule(VkDevice, const VkShaderMo return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) { - --mock.live; +VKAPI_ATTR void VKAPI_CALL mockDestroyShaderModule(VkDevice, VkShaderModule module, const VkAllocationCallbacks*) { + if (module != VK_NULL_HANDLE) --mock.live; } VKAPI_ATTR VkResult VKAPI_CALL mockCreateComputePipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkComputePipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { @@ -2504,6 +2849,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkUpdateDescriptorSets", reinterpret_cast(mockUpdateDescriptorSets)}, {"vkCmdBindDescriptorSets", reinterpret_cast(mockCmdBindDescriptorSets)}, + {"vkCreateGraphicsPipelines", reinterpret_cast(mockCreateGraphicsPipelines)}, {"vkCreatePipelineLayout", reinterpret_cast(mockCreatePipelineLayout)}, {"vkDestroyPipelineLayout", reinterpret_cast(mockDestroyPipelineLayout)}, {"vkCreateShaderModule", reinterpret_cast(mockCreateShaderModule)}, @@ -3165,6 +3511,9 @@ struct ModuleShape { bool fragDepth = false; std::uint32_t sampleMaskLength = 0; std::uint32_t floatControlsWidth = 0; + std::uint32_t clipDistanceLength = 0; + std::uint32_t cullDistanceLength = 0; + bool layerOutput = false; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -3203,6 +3552,14 @@ std::vector makeModule(const ModuleShape& shape) { emit(annotations, spv::OpDecorate, {parameter, spv::DecorationLocation, shape.parameterLocation}); extraInterface.push_back(parameter); } + if (shape.layerOutput) { + const auto pointer = id(); + const auto layer = id(); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, uintType}); + emit(declarations, spv::OpVariable, {pointer, layer, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {layer, spv::DecorationBuiltIn, spv::BuiltInLayer}); + extraInterface.push_back(layer); + } if (shape.secondTarget) { const auto target = id(); emit(declarations, spv::OpVariable, {outputPointer, target, spv::StorageClassOutput}); @@ -3264,6 +3621,30 @@ std::vector makeModule(const ModuleShape& shape) { emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInSampleMask}); extraInterface.push_back(variable); } + if (shape.clipDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.clipDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance}); + extraInterface.push_back(variable); + } + if (shape.cullDistanceLength != 0) { + const auto length = id(); + const auto array = id(); + const auto pointer = id(); + const auto variable = id(); + emit(declarations, spv::OpConstant, {uintType, length, shape.cullDistanceLength}); + emit(declarations, spv::OpTypeArray, {array, floatType, length}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInCullDistance}); + extraInterface.push_back(variable); + } if (shape.perVertex) { const auto length = id(); const auto array = id(); @@ -3333,6 +3714,17 @@ std::vector makeModule(const ModuleShape& shape) { emit(words, spv::OpCapability, {spv::CapabilityShader}); if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading}); if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry}); + if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance}); + if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance}); + if (shape.layerOutput) { + emit(words, spv::OpCapability, {spv::CapabilityShaderViewportIndexLayerEXT}); + const std::string extension = "SPV_EXT_shader_viewport_index_layer"; + const auto count = (extension.size() + 4) / 4; + words.push_back((static_cast(count + 1) << 16u) | spv::OpExtension); + const auto start = words.size(); + words.resize(start + count, 0); + for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast(static_cast(extension[i])) << ((i % 4) * 8); + } if (shape.barycentric) { emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR}); const std::string extension = "SPV_KHR_fragment_shader_barycentric"; @@ -3360,6 +3752,61 @@ std::vector makeModule(const ModuleShape& shape) { return words; } +void pipelineLibrarySetTests() { + using namespace AgcDriver::Graphics; + for (const auto mask : {0u, 1u, 2u, 3u}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + for (std::uint32_t set = 0; set < 2; ++set) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.descriptorSet = set; + if ((mask & (1u << set)) != 0u) fragment.bindings.push_back(binding); + else vertex.bindings.push_back(binding); + } + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{11}, fragmentKey{22}; + PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + const auto prebuild = [&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); }; + prebuild(); + for (const auto part : {VK_GRAPHICS_PIPELINE_LIBRARY_PRE_RASTERIZATION_SHADERS_BIT_EXT, VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT}) { + const auto layout = mock.libraryLayouts.at(part); + const auto expectedMask = part == VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT ? mask : mask ^ 3u; + const std::vector expected{(expectedMask & 1u) != 0u ? resources.main : VK_NULL_HANDLE, (expectedMask & 2u) != 0u ? resources.fragment : VK_NULL_HANDLE}; + Require(mock.pipelineLayoutSets.at(layout) == expected, "shader library lost a used set or retained an unused set"); + Require(mock.pipelineLayoutFlags.at(layout) == VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT, "shader library lost independent sets"); + const auto& pushes = mock.pipelineLayoutPushRanges.at(layout); + Require(pushes.size() == 2 && pushes[0].offset == 0 && pushes[0].size == PipelinePushSlotBytes && pushes[0].stageFlags == VK_SHADER_STAGE_VERTEX_BIT && pushes[1].offset == PipelinePushSlotBytes && pushes[1].size == PipelinePushSlotBytes && pushes[1].stageFlags == VK_SHADER_STAGE_FRAGMENT_BIT, "shader library changed push constant slots"); + } + auto before = PipelineLibraryCountersOf(context.device).built; + prebuild(); + Require(PipelineLibraryCountersOf(context.device).built == before, "identical shader libraries were rebuilt"); + for (std::uint32_t set = 0; set < 2; ++set) { + if (set == 0) { ++mainKey[0]; resources.main = makeHandle(); } + else { ++fragmentKey[0]; resources.fragment = makeHandle(); } + prebuild(); + const auto after = PipelineLibraryCountersOf(context.device).built; + Require(after[0] == before[0] && after[3] == before[3], "descriptor layout change rebuilt interface libraries"); + Require(after[1] == before[1] + ((mask & (1u << set)) == 0u ? 1u : 0u), "pre-rasterization library reuse ignored descriptor set usage"); + Require(after[2] == before[2] + ((mask & (1u << set)) != 0u ? 1u : 0u), "fragment library reuse ignored descriptor set usage"); + before = after; + } + ShaderRecompiler::DescriptorBinding invalid{}; + invalid.descriptorSet = 32; + fragment.bindings.push_back(invalid); + expectFailure(prebuild, "descriptor set exceeds two slots"); + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "shader library tests leaked Vulkan objects"); + } +} + void barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -3466,6 +3913,99 @@ void barycentricEmulationTests() { expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } +void copiedWriterIndirectOverlapTests() { + using namespace ShaderRecompiler; + using namespace AgcDriver::Graphics; + + constexpr std::size_t half = 65536; + + void* directBlock = ::operator new(half, std::align_val_t{half}); + std::memset(directBlock, 0xaa, half); + const auto directAddress = reinterpret_cast(directBlock); + + void* copiedBlock = ::operator new(half, std::align_val_t{half}); + std::memset(copiedBlock, 0xbb, half); + const auto copiedAddress = reinterpret_cast(copiedBlock); + + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, directBlock, half, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + + mock = MockVulkan{}; + auto context = mockContext(); + context.hostImportAlignment = half; + context.limits.minStorageBufferOffsetAlignment = 16; + context.limits.maxStorageBufferRange = 1u << 27; + + { + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + Require(HostImportCovers(context, directAddress, half), "direct registered range must be covered by host import"); + Require(!HostImportCovers(context, copiedAddress, half), "copied unregistered range must NOT be covered by host import"); + + memory.AddWritable(copiedAddress, 1024); + memory.AddWritable(directAddress, 1024); + memory.Upload(false); + + Require(memory.HasCopiedWrites(), "writer with one copied region must report HasCopiedWrites() == true"); + + Require(memory.WritesOverlap(directAddress, 16), "WritesOverlap must return true for direct written region"); + Require(memory.WritesOverlap(copiedAddress, 16), "WritesOverlap must return true for copied written region"); + Require(!memory.WritesOverlap(directAddress + 2048, 16), "WritesOverlap must return false for unwritten range"); + + Require(!memory.CopiedWritesOverlap(directAddress, 16), "CopiedWritesOverlap must return false for direct written region"); + Require(memory.CopiedWritesOverlap(copiedAddress, 16), "CopiedWritesOverlap must return true for copied written region"); + Require(!memory.CopiedWritesOverlap(directAddress + 2048, 16), "CopiedWritesOverlap must return false for unwritten range"); + + memory.WriteBack(); + } + + { + RecompileResult shader; + shader.runtimeAbiVersion = RuntimeAbi::Version; + auto copiedBinding = makeBinding(Role::GuestBuffers, 0, 1, vsharp(copiedBlock, 1024)); + copiedBinding.bufferWritten = {true}; + auto directBinding = makeBinding(Role::GuestBuffers, 1, 1, vsharp(directBlock, 1024)); + directBinding.bufferWritten = {true}; + shader.bindings = {copiedBinding, directBinding}; + CompiledShader compiled{ShaderStage::Compute, &shader, 0}; + auto writer = std::make_shared(context, compiled); + + Require(writer->HasCopiedWrites(), "ShaderResources with copied region must report HasCopiedWrites() == true"); + Require(writer->WritesOverlap(directAddress, 16), "ShaderResources must report WritesOverlap for direct region"); + Require(!writer->CopiedWritesOverlap(directAddress, 16), "ShaderResources must NOT report CopiedWritesOverlap for direct region"); + Require(writer->CopiedWritesOverlap(copiedAddress, 16), "ShaderResources must report CopiedWritesOverlap for copied region"); + + auto copiedWriters = DrawCopiedWriters(); + copiedWriters->push_back(writer); + + const auto overlapsMain = [&](const auto& w) { return w->WritesOverlap(directAddress, 16); }; + const auto overlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(directAddress, 16); }; + + const bool mainOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsMain); + const bool prOverlaps = std::any_of(copiedWriters->begin(), copiedWriters->end(), overlapsPR); + + Require(mainOverlaps, "main's check (WritesOverlap) must flag an overlap over direct arguments"); + Require(!prOverlaps, "PR's check (CopiedWritesOverlap) must disambiguate and NOT flag an overlap over direct arguments"); + + const auto copiedOverlapsPR = [&](const auto& w) { return w->CopiedWritesOverlap(copiedAddress, 16); }; + Require(std::any_of(copiedWriters->begin(), copiedWriters->end(), copiedOverlapsPR), "PR's check must flag an overlap over copied arguments"); + + copiedWriters->erase(std::remove(copiedWriters->begin(), copiedWriters->end(), writer), copiedWriters->end()); + writer->WriteBack(); + } + + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, directBlock); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(directBlock, std::align_val_t{half}); + ::operator delete(copiedBlock, std::align_val_t{half}); +} + void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -3667,6 +4207,41 @@ void conservativeZExportTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); } +bool declaresEarlyFragmentTests(const ShaderRecompiler::RecompileRequest& request) { + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + } + return false; +} + +void earlyFragmentTestsTests() { + const std::array, 9> cases{{ + {0x0000u, false}, + {0x0020u, false}, + {0x0010u, true}, + {0x0030u, true}, + {0x0050u, false}, + {0x0011u, false}, + {0x1000u, true}, + {0x1050u, true}, + {0x1031u, true} + }}; + const ShaderRecompiler::RequestSerializer serializer; + for (const auto& [shaderControl, expected] : cases) { + const auto request = depthExportRequest(shaderControl); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(declaresEarlyFragmentTests(request) == expected && declaresEarlyFragmentTests(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + (expected ? " did not declare" : " declared") + " early fragment tests"); + } + for (const auto shaderControl : {0x0010u, 0x1000u}) { + auto request = depthExportRequest(shaderControl); + request.context.pixel->alphaToCoverage = true; + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests"); + } +} + constexpr std::array OrderedPixelCode{0x7e0e02f2u, 0xbf900007u, 0xf800180fu, 0x07070707u, 0xbf810000u}; constexpr std::array OrderedBdaPixelCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf800180fu, 0x07070707u, 0xbf810000u}; @@ -3870,6 +4445,7 @@ void stencilRefExportTests() { Require(!AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck accepted STENCIL_OP_VAL_EXPORT_ENABLE in DB_SHADER_CONTROL " + std::to_string(shaderControl)); } Require(!AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x50u, 2u).context, IdentityExports).stencilRefExportEnable, "a pixel stage without STENCIL_TEST_VAL_EXPORT_ENABLE exports the stencil reference"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(stencilRefExportQueue(0x1012u, 2u).context, IdentityExports).earlyZ, "DEPTH_BEFORE_SHADER with a stencil reference export did not get early tests"); const std::array stencilExport{spv::CapabilityShader, spv::CapabilityStencilExportEXT}; const std::array plain{spv::CapabilityShader}; const ShaderRecompiler::RequestSerializer serializer; @@ -3927,6 +4503,71 @@ void stencilRefExportTests() { static_cast(AgcDriver::Graphics::DecodeState(queue)); } +std::set bufferViews(std::span words) { + std::set views; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) != spv::OpName) continue; + const std::string name(reinterpret_cast(&words[at + 2])); + if (name.starts_with("buffers")) views.insert(name); + } + return views; +} + +void aliasedBufferViewTests() { + constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; + constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + constexpr std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu}; + const auto code = [](std::uint32_t store, std::uint32_t resource) { + return std::array{0xe0000000u, 0x80010a00u, 0xbf8c3f70u, store, resource, 0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + }; + const std::array, std::set>, 2> cases{{ + {code(0xe0104000u, 0x80010a00u), {"buffers", "buffers_coherent"}}, + {code(0xe1480000u, 0x80010a00u), {"buffers", "buffers_u64"}}, + }}; + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + for (const auto& [words, views] : cases) { + auto request = orderedPixelRequest(0x20600u, capabilities, words); + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + auto pixel = ShaderRecompiler::Recompile(request); + const auto declared = bufferViews(pixel.spirv.Words()); + std::string names; + for (const auto& name : declared) names += " " + name; + 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(); + 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; + } + pixel.spirv = std::move(unaliased); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); }, "duplicate SPIR-V resource binding"); + } +} + +void LineStripTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.userConfig[0x242] = 3; + auto state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && !state.rectList && !state.primitiveRestart, "primitive type 3 did not decode as a line strip"); + Require(RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a line strip draw did not get the line primitive class"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + state = DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP && state.primitiveRestart, "primitive restart was lost on a line strip"); + const auto rejection = DrawRejection(queue, true); + Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -3935,7 +4576,7 @@ void ProvokingVertexTests() { queue.context[0x1b4] = 2; const auto first = DecodeState(queue); Require(first.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT, "the default provoking vertex is not first"); - for (const auto primitive : {2u, 4u, 5u, 6u}) { + for (const auto primitive : {2u, 3u, 4u, 5u, 6u}) { queue.userConfig[0x242] = primitive; for (const auto last : {false, true, false}) { queue.context[0x205] = 0x240u | (last ? lastVertex : 0u); @@ -4427,6 +5068,19 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, false); }, "unsupported device capability 35"); pixel.spirv = makeModule({.fragment = true, .sampleMaskLength = 2}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment built-in"); + pixel.spirv = makeModule({.fragment = true}); + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4, .cullDistanceLength = 4}); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); + state.paClVsOutCntl = 0x0040000fu; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); + vertex.spirv = makeModule({.parameterOutput = true, .cullDistanceLength = 4}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); }, "PA_CL_VS_OUT_CNTL=0x0040000f"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); + state.paClVsOutCntl = 0; + vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 9}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); }, "clip or cull distance"); vertex.spirv = makeModule({.parameterOutput = true, .sampleId = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 35"); vertex.spirv = makeModule({.parameterOutput = true}); @@ -4461,6 +5115,7 @@ void validationTests() { vertex.vertexAttributes[0].components = 2; expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range"); + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 64, "vertex range includes records before the first vertex"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); { auto wider = attribute; @@ -4483,6 +5138,7 @@ void validationTests() { Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); } attribute.fetchIndex = 1; + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 2, 7) == 224, "instance range includes records before the first instance"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 2, 2) == 112, "a first instance past the records still spans its fetch"); @@ -4500,6 +5156,7 @@ void validationTests() { attribute.resource.fields[1] = 0; attribute.resource.fields[2] = 16; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 16, "zero stride must repeat one value"); + Require(AgcDriver::Graphics::VertexBufferReadOffset(attribute, 100, 7) == 0, "zero-stride input skipped its repeated value"); attribute.resource.fields[2] = 8; expectFailure([&] { AgcDriver::Graphics::VertexBufferReadSize(attribute, 0, 1); }, "byte range"); Require(!AgcDriver::Graphics::VertexFetchOutOfRange(attribute), "a structured zero-stride fetch was treated as out of range"); @@ -4575,6 +5232,14 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 12"); AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, true); } + { + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({.layerOutput = true}); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "VK_EXT_shader_viewport_index_layer"); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true); + } for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) { ShaderRecompiler::RecompileResult vertex; vertex.spirv = makeModule({}); @@ -4917,6 +5582,19 @@ int main(int argc, char** argv) { setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { + if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { + bool passed = false; + { + auto context = mockContext(); + context.limits.maxStorageBufferRange = 1u << 27; + passed = RunSparseBufferHoleTests(context, { + [](VkBuffer buffer) -> std::span { return mockBufferMemory(buffer); }, {}, {}, {}, {} + }); + } + Require(mock.live == 0, "sparse buffer hole tests leaked Vulkan objects"); + std::cout << "sparse buffer hole tests " << (passed ? "passed" : "failed") << '\n'; + return passed ? 0 : 1; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { descriptorLayoutFlagTests(); std::cout << "Descriptor layout native and captured binding flags passed\n"; @@ -4927,6 +5605,11 @@ int main(int argc, char** argv) { std::cerr << "descriptor pool reset failure did not terminate\n"; return 1; } + if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") { + multisampleTests(); + std::cout << "Covered-centroid and multisample state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; @@ -4959,6 +5642,7 @@ int main(int argc, char** argv) { } RunGuestLeaseWaitTests(); stateTests(); + pipelineLibrarySetTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); @@ -4967,12 +5651,16 @@ int main(int argc, char** argv) { DepthStencilTests(); OneDimensionalColorTests(); htileDepthClearTests(); + memoryValueFillTests(); ZExportTests(); unorm10_11_11TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); + earlyFragmentTestsTests(); orderedPixelShaderTests(); stencilRefExportTests(); + aliasedBufferViewTests(); + LineStripTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); @@ -4989,6 +5677,9 @@ int main(int argc, char** argv) { uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + clipDistanceTests(); + framebufferLayerTests(); + UnwrittenSampleLocationTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); @@ -5026,6 +5717,7 @@ int main(int argc, char** argv) { rectListTests(); floatControlsModeTests(); barycentricEmulationTests(); + copiedWriterIndirectOverlapTests(); mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp index ad2314a044..dd5b45a647 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,35 @@ void reject(TAction action) { throw std::runtime_error("expected guest allocation ownership rejection"); } +void leaseSnapshotTests() { + std::array memory{}; + const auto address = reinterpret_cast(memory.data()); + { + GuestAllocations::Mutation mutation; + mutation.Add(memory.data(), 64, true, true, true); + } + std::weak_ptr held; + { + const auto first = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + const auto second = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(!first.empty() && first.size() == second.size() && &first[0] == &second[0], "two leases taken without a registry change do not share one snapshot"); + const auto found = std::find_if(first.begin(), first.end(), [&](const auto& range) { return range->address == address; }); + Require(found != first.end(), "the shared snapshot misses a registered range"); + held = *found; + Require(held.use_count() == 2, "the shared snapshot of two leases holds a range more than once"); + { + GuestAllocations::Mutation mutation; + mutation.Add(memory.data() + 64, 64, true, true, true); + } + const auto third = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + Require(&third[0] != &first[0] && std::any_of(third.begin(), third.end(), [&](const auto& range) { return range->address == address + 64; }), "a lease taken after a registry change still shares the old snapshot"); + } + Require(held.use_count() == 1, "a snapshot outlived its leases"); + GuestAllocations::Mutation mutation; + mutation.Remove(memory.data()); + mutation.Remove(memory.data() + 64); +} + } void RunGuestLeaseWaitTests() { @@ -38,8 +68,10 @@ void RunGuestLeaseWaitTests() { mutation.Add(memory.data(), 64, true, true, true); mutation.Add(memory.data() + 64, 64, true, true, true); } - auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - std::erase_if(lease, [&](const auto& range) { return range->address != address; }); + GuestAllocations::Lease::Ranges lease; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address) lease.push_back(range); + } std::atomic mutationEntered = false; std::atomic lookupFinished = false; std::exception_ptr failure; @@ -56,8 +88,10 @@ void RunGuestLeaseWaitTests() { } catch (...) { failure = std::current_exception(); } }); mutationEntered.wait(false); - auto other = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - std::erase_if(other, [&](const auto& range) { return range->address != address + 64; }); + GuestAllocations::Lease::Ranges other; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address + 64) other.push_back(range); + } const bool found = other.size() == 1 && other.front()->address == address + 64; lookupFinished.store(true); lease.clear(); @@ -74,6 +108,7 @@ void RunGuestLeaseWaitTests() { } void RunGuestAllocationTests() { + leaseSnapshotTests(); void* pointer = GuestHeap::GuestHeapAllocate_nid_postfix(32); Require(reinterpret_cast(pointer) % alignof(std::max_align_t) == 0, "guest malloc is not suitably aligned"); std::memset(pointer, 0x55, 32); diff --git a/core/libs/prx/libSceAgcDriver/tests/IntegerNarrowing.cpp b/core/libs/prx/libSceAgcDriver/tests/IntegerNarrowing.cpp new file mode 100644 index 0000000000..cd69416957 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/IntegerNarrowing.cpp @@ -0,0 +1,118 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +enum class Bitcast { None, F32, F16 }; + +struct RoundTrip { + const char* name; + unsigned bits; + Bitcast bitcast; +}; + +IrValue& convert(IrBuilder& builder, IrValue& source, const RoundTrip& test) { + if (test.bitcast == Bitcast::F16) { + return builder.BitCastU32FromF16(builder.BitCastF16(source)); + } + auto& input = test.bitcast == Bitcast::F32 ? builder.BitCastU32(builder.BitCastF32(source)) : source; + auto& narrowed = builder.Emit(test.bits == 16 ? IrOpcode::ConvertU16U32 : IrOpcode::ConvertU8U32, + test.bits == 16 ? IrType::U16 : IrType::U8, {&input}); + auto& widened = builder.Emit(test.bits == 16 ? IrOpcode::ConvertU32U16 : IrOpcode::ConvertU32U8, + IrType::U32, {&narrowed}); + return test.bitcast == Bitcast::F32 ? builder.BitCastU32(builder.BitCastF32(widened)) : widened; +} + +void fold(IrProgram& program, bool cleanup) { + ConstantFolder().Fold(program); + if (cleanup) { + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + } + ValidateProgram(program, true); +} + +bool checkRoundTrip(const RoundTrip& test, std::uint32_t pattern, std::uint32_t laneValue, bool dynamic, bool cleanup) { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0; + program.Metadata().blockInfo.push_back(info); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + IrValue* lane = dynamic ? &builder.Emit(IrOpcode::LaneId, IrType::U32, {}) : nullptr; + auto& source = dynamic ? builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(pattern)}) + : builder.Constant(pattern); + auto& result = convert(builder, source, test); + auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result}); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ValidateProgram(program, true); + fold(program, cleanup); + if (dynamic) { + if (user.Argument(0)->Resolve()->HasImmediate() || !lane->HasUses()) { + std::fprintf(stderr, "%s: optimized result must still depend on LaneId\n", test.name); + return false; + } + lane->ReplaceUsesWith(&builder.Constant(laneValue), true); + fold(program, cleanup); + } + const auto expected = (pattern + laneValue) & (test.bits == 16 ? 0xffffu : 0xffu); + const auto* actual = user.Argument(0)->Resolve(); + if (!actual->HasImmediate() || actual->Type() != IrType::U32 || actual->ImmediateU32() != expected) { + std::fprintf(stderr, "%s: pattern=%08x lane=%u dynamic=%d cleanup=%d expected=%08x actual=%08x immediate=%d\n", + test.name, pattern, laneValue, dynamic, cleanup, expected, + actual->HasImmediate() ? actual->ImmediateU32() : 0u, actual->HasImmediate()); + return false; + } + return true; +} + +} + +int main() { + try { + constexpr RoundTrip cases[] = { + {"U16 round trip", 16, Bitcast::None}, + {"U8 round trip", 8, Bitcast::None}, + {"U16 with F32 bitcasts", 16, Bitcast::F32}, + {"U8 with F32 bitcasts", 8, Bitcast::F32}, + {"F16 bitcast round trip", 16, Bitcast::F16}, + }; + constexpr std::uint32_t patterns[] = { + 0u, 1u, 0x7fu, 0x80u, 0xffu, 0x100u, 0x7fffu, 0x8000u, + 0xffffu, 0x10000u, 0x12345678u, 0x80000000u, 0xffff0000u, 0xffffff00u, 0xffffffffu, + }; + constexpr std::uint32_t lanes[] = {0u, 1u, 7u, 31u, 32u, 63u}; + unsigned total = 0; + unsigned failures = 0; + for (const auto& test : cases) { + for (auto pattern : patterns) { + ++total; + failures += !checkRoundTrip(test, pattern, 0u, false, true); + for (auto lane : lanes) { + for (bool cleanup : {false, true}) { + ++total; + failures += !checkRoundTrip(test, pattern, lane, true, cleanup); + } + } + } + } + std::printf("Integer narrowing: %u cases, %u failures\n", total, failures); + return failures == 0 ? 0 : 1; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp new file mode 100644 index 0000000000..86fd98da3e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/IrReclaim.cpp @@ -0,0 +1,93 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + try { + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& value = builder.Constant(42u); + IrSsaValues table; + const auto& readTable = table; + if (readTable[0] != nullptr || table.HasStorage()) throw std::runtime_error("an empty SSA table allocated storage during a read"); + for (IrValue* entry : readTable) if (entry != nullptr) throw std::runtime_error("an empty SSA table contained a value"); + if (table.HasStorage()) throw std::runtime_error("iterating an empty SSA table allocated storage"); + table[3] = &value; + if (!table.HasStorage() || readTable[3] != &value || readTable[2] != nullptr) throw std::runtime_error("an SSA write lost zero initialization or its value"); + table.fill(nullptr); + if (table.HasStorage() || readTable[3] != nullptr) throw std::runtime_error("clearing an SSA table retained construction storage"); + const auto id = value.Id(); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&value})); + auto& identity = program.CreateValue(IrOpcode::Identity, IrType::U32); + identity.AddArgument(&value); + block.AppendInstruction(&identity); + DeadCodeEliminator{}.RemoveIdentities(program); + auto& first = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + auto& second = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + first.AddArgument(&second); + second.AddArgument(&first); + std::vector pinned; + for (std::uint32_t number = 100u; number < 112u; ++number) pinned.push_back(&builder.Constant(number)); + block.ssaScalarValues[0] = pinned[0]; + block.ssaVectorValues[0] = pinned[1]; + block.ssaThreadBitScalarValues[0] = pinned[2]; + block.ssaScalarMaskTags[0] = pinned[3]; + program.Metadata().dynamicReads.push_back(pinned[4]); + program.Metadata().blockInfo.emplace_back(); + program.Metadata().blockInfo.back().condition = pinned[5]; + program.Metadata().blockInfo.back().indirectTarget = pinned[6]; + auto& resources = program.Resources(); + resources.descriptorSources.emplace_back(); + resources.descriptorSources.back().dwords[0] = pinned[7]; + resources.srtReads.push_back({pinned[8], 0u}); + resources.controlFlow.emplace_back(); + resources.controlFlow.back().condition = pinned[9]; + resources.uniformFill.values[0] = pinned[10]; + auto external = std::make_unique(IrOpcode::Identity, IrType::U32, value.Id()); + external->AddArgument(pinned[11]); + resources.valueStorage.push_back(std::move(external)); + const auto removed = program.ReclaimUnusedValues(); + if (removed != 3u) throw std::runtime_error("detached identity and cyclic garbage were not reclaimed"); + if (value.Id() != id || value.ImmediateU32() != 42u || value.Uses().size() != 1u) throw std::runtime_error("a live instruction lost its value or retained an unused backlink"); + for (std::size_t index = 0; index < pinned.size(); ++index) { + if (pinned[index]->ImmediateU32() != 100u + index) throw std::runtime_error("a metadata or resource-owned value was reclaimed"); + } + if (program.ReclaimUnusedValues() != 0u) throw std::runtime_error("reclaiming an unchanged graph was not idempotent"); + auto& unused = program.CreateValue(IrOpcode::IAdd32, IrType::U32); + IrValue untracked(IrOpcode::Identity, IrType::U32, value.Id()); + untracked.AddArgument(&unused); + bool rejected = false; + try { + static_cast(program.ReclaimUnusedValues()); + } catch (const std::runtime_error&) { + rejected = true; + } + if (!rejected) throw std::runtime_error("an untracked external owner was accepted"); + untracked.ReplaceArgument(0, nullptr); + program.Metadata().dynamicReads.clear(); + if (program.ReclaimUnusedValues() != 2u) throw std::runtime_error("reclamation did not recover after an external-owner rejection"); + if (builder.Constant(999u).Id() <= pinned.back()->Id()) throw std::runtime_error("reclamation reused a value ID"); + IrProgram moved = std::move(program); + if (value.Id() != id || value.ImmediateU32() != 42u || moved.ReclaimUnusedValues() != 1u) throw std::runtime_error("moving a program invalidated its allocation owner"); + IrProgram assigned; + static_cast(assigned.CreateValue(IrOpcode::IAdd32, IrType::U32)); + assigned = std::move(moved); + if (value.Id() != id || value.ImmediateU32() != 42u || assigned.ReclaimUnusedValues() != 0u) throw std::runtime_error("move assignment invalidated a live allocation owner"); + if (assigned.CreateValue(IrOpcode::IAdd32, IrType::U32).Id() <= pinned.back()->Id()) throw std::runtime_error("a transferred allocation owner reused a value ID"); + std::puts("IR reclamation ownership tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp new file mode 100644 index 0000000000..25c9a62bed --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -0,0 +1,221 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp" +#include "zlib.h" +#include +#include +#include + +namespace { + +using AgcDriver::TraceFormat; +using AgcDriver::TraceThread; +using AgcDriver::TraceWindow; +using Clock = std::chrono::steady_clock; +using Edge = TraceWindow::Edge; +using std::chrono::microseconds; +using std::chrono::milliseconds; + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void WindowByFrames() { + auto window = TraceWindow::Parse("3:2", nullptr).value(); + std::vector edges; + for (std::uint64_t present = 0; present < 8; ++present) edges.push_back(window.Present(present, std::chrono::seconds(present))); + Check(edges == std::vector{Edge::None, Edge::None, Edge::None, Edge::Open, Edge::None, Edge::Close, Edge::None, Edge::None}, "frame window opened or closed at the wrong present"); + Check(window.Describe() == "frames 3:2", "frame window description changed"); +} + +void WindowBySeconds() { + auto window = TraceWindow::Parse(nullptr, "2:1").value(); + std::vector edges; + for (const auto elapsed : {milliseconds(0), milliseconds(1999), milliseconds(2000), milliseconds(2999), milliseconds(3000), milliseconds(5000)}) + edges.push_back(window.Present(0, elapsed)); + Check(edges == std::vector{Edge::None, Edge::None, Edge::Open, Edge::None, Edge::Close, Edge::None}, "seconds window opened or closed at the wrong present"); + Check(window.Describe() == "seconds 2:1", "seconds window description changed"); +} + +void WindowRejects() { + Check(!TraceWindow::Parse(nullptr, nullptr).has_value(), "a window was made without a switch"); + for (const auto* text : {"5", "a:1", "1:0", "1:", ":1", "1:2:3", "-1:2"}) { + bool rejected = false; + try { TraceWindow::Parse(text, nullptr); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "a malformed window was accepted"); + } + bool rejected = false; + try { TraceWindow::Parse("1:1", "1:1"); } + catch (const std::invalid_argument&) { rejected = true; } + Check(rejected, "frames and seconds were accepted together"); +} + +void BufferCap() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == 3 && snapshot.totals[0].length == microseconds(90), "totals missed a slice past the cap"); +} + +void FoldShortSlices() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 16); + const auto origin = Clock::now(); + thread.Add("Driver.Packet", nullptr, origin, origin + microseconds(5)); + thread.Add("Driver.Packet", "draw", origin + microseconds(5), origin + microseconds(10)); + thread.Add("Driver.Draw", nullptr, origin + microseconds(10), origin + microseconds(40)); + presents.store(2); + thread.Add("Driver.Completion", nullptr, origin + microseconds(50), origin + microseconds(90)); + const auto snapshot = thread.Collect(); + Check(snapshot.slices.size() == 3 && !snapshot.dropped, "short slices were written or long ones lost"); + Check(std::string_view(snapshot.slices[0].scope) == "Driver.Draw", "the first long slice is missing"); + Check(snapshot.slices[1].scope == nullptr && snapshot.slices[1].begin == origin && snapshot.slices[1].length == microseconds(10), "the folded sample of the previous present is wrong"); + Check(std::string_view(snapshot.slices[2].scope) == "Driver.Completion", "the slice after the present is missing"); + Check(snapshot.totals.size() == 4, "totals did not keep scope and stage apart"); + for (const auto& total : snapshot.totals) Check(total.count == 1, "a total counted a slice twice"); +} + +void EscapeStrings() { + std::string output; + TraceFormat::AppendString(output, "a\"b\\c\n\xe9"); + Check(output == "\"a\\\"b\\\\c\\u000a\\u00e9\"", "string escaping is not valid JSON"); +} + +void EventsShape() { + const auto origin = Clock::now(); + const std::vector slices{ + {"Driver.Packet", nullptr, origin + microseconds(10), microseconds(30)}, + {"Driver.Packet", "draw", origin + microseconds(12), microseconds(25)}, + {nullptr, nullptr, origin, milliseconds(2)}, + }; + const std::vector threads{{"queue worker 0x0", slices, {}, false}}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"traceEvents\":[\n" + "{\"name\":\"process_name\",\"ph\":\"M\",\"ts\":0.000,\"pid\":7,\"tid\":0,\"args\":{\"name\":\"AnyPS5\"}},\n" + "{\"name\":\"thread_name\",\"ph\":\"M\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"args\":{\"name\":\"queue worker 0x0\"}},\n" + "{\"name\":\"queue worker 0x0 folded\",\"ph\":\"C\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"args\":{\"ms\":2.000}},\n" + "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":10.000,\"pid\":7,\"tid\":1,\"dur\":30.000},\n" + "{\"name\":\"Driver.Packet.draw\",\"ph\":\"X\",\"ts\":12.000,\"pid\":7,\"tid\":1,\"dur\":25.000},\n" + "{\"name\":\"present\",\"ph\":\"i\",\"ts\":0.000,\"pid\":7,\"tid\":1,\"s\":\"g\",\"args\":{\"present\":5}}\n" + "]}\n"; + Check(json == expected, "trace events changed shape"); +} + +void SummaryFields() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.commit = "abc"; + summary.dirty = false; + summary.environment = {{"APS5_TRACE_FRAMES", "3:2"}}; + summary.gpu = "GPU"; + summary.driver = "Driver 1"; + summary.os = "OS 1"; + summary.width = 1920; + summary.height = 1080; + summary.window = "frames 3:2"; + summary.firstPresent = 3; + summary.gpuBusyMs = 20; + summary.trace = "aps5-trace-abc-7.json.gz"; + const std::vector threads{ + {"queue worker 0x0", {}, {{"Driver.Packet", nullptr, milliseconds(12), 4}, {"Driver.WorkerWait", nullptr, milliseconds(8), 4}, {"Driver.Completion", nullptr, milliseconds(2), 4}, {"Driver.Packet", "draw", milliseconds(10), 2}}, false}, + {"presenter", {}, {{"Driver.Present", nullptr, milliseconds(6), 2}, {"Driver.Present", "inflight_wait", milliseconds(4), 2}}, 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\":false,\"env\":{\"APS5_TRACE_FRAMES\":\"3:2\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\"," + "\"resolution\":\"1920x1080\",\"window\":{\"spec\":\"frames 3:2\",\"first_present\":3,\"ms\":40.000},\"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\":35.000}," + "\"top_phases_ms_per_frame\":{\"Driver.Packet\":6.000,\"Driver.Packet.draw\":5.000,\"Driver.Present\":3.000,\"Driver.Present.inflight_wait\":2.000,\"Driver.Completion\":1.000}," + "\"trace_dropped\":false,\"trace\":\"aps5-trace-abc-7.json.gz\"}\n"; + Check(line == expected, "summary line changed"); + auto dropped = threads; + dropped[1].dropped = true; + Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); +} + +void GpuClock() { + const auto origin = Clock::now(); + const AgcDriver::TraceClockSample first{1000.0, origin + microseconds(10)}; + const AgcDriver::TraceClockSample last{41000.0, origin + microseconds(30)}; + const std::vector ranges{{"batch", 5000.0, 9000.0}, {"draw", 6000.0, 7000.0}, {"batch", -39000.0, -30000.0}, {"copy", 8000.0, 8000.0}}; + const auto slices = TraceFormat::GpuSlices(ranges, first, last, origin); + Check(slices.size() == 2, "a GPU range before the window or without length was kept"); + Check(std::string_view(slices[0].scope) == "batch" && slices[0].stage == nullptr && slices[0].begin == origin + microseconds(12) && slices[0].length == microseconds(2), "a GPU range was not mapped between the two calibrations"); + Check(std::string_view(slices[1].scope) == "draw" && slices[1].begin == origin + std::chrono::nanoseconds(12500) && slices[1].length == std::chrono::nanoseconds(500), "a nested GPU range was not mapped"); + const AgcDriver::TraceClockSample same{1000.0, origin + microseconds(10)}; + Check(TraceFormat::GpuSlices(ranges, same, same, origin)[0].begin == origin + microseconds(14), "one calibration does not map at the CPU rate"); +} + +void GzipFile() { + std::string text; + for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; + const auto stem = "aps5-trace-test-" + std::to_string(Clock::now().time_since_epoch().count()); + const auto path = std::filesystem::temp_directory_path() / (stem + ".json.gz"); + AgcDriver::TraceFile::WriteGzip(path, text); + std::ifstream input(path, std::ios::binary); + const std::vector bytes((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + input.close(); + std::filesystem::remove(path); + Check(bytes.size() > 18 && bytes[0] == 0x1f && bytes[1] == 0x8b && bytes[2] == 8 && bytes[3] == 0, "the gzip header is wrong"); + z_stream stream{}; + Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed"); + std::string inflated(text.size() + 1, 0); + stream.next_in = const_cast(bytes.data() + 10); + stream.avail_in = static_cast(bytes.size() - 18); + stream.next_out = reinterpret_cast(inflated.data()); + stream.avail_out = static_cast(inflated.size()); + const int status = inflate(&stream, Z_FINISH); + inflated.resize(inflated.size() - stream.avail_out); + const bool consumed = stream.avail_in == 0; + inflateEnd(&stream); + Check(status == Z_STREAM_END && consumed, "the deflate stream does not end at the trailer"); + Check(inflated == text, "the gzip file does not hold the text"); + const auto word = [&](std::size_t at) { + std::uint32_t value = 0; + for (unsigned index = 0; index < 4; ++index) value |= static_cast(bytes[at + index]) << (8 * index); + return value; + }; + Check(word(bytes.size() - 8) == crc32(0, reinterpret_cast(text.data()), static_cast(text.size())), "the CRC-32 is wrong"); + Check(word(bytes.size() - 4) == text.size(), "the size is wrong"); +} + +void AppendLines() { + const auto path = std::filesystem::temp_directory_path() / ("aps5-summary-test-" + std::to_string(Clock::now().time_since_epoch().count()) + ".jsonl"); + AgcDriver::TraceFile::AppendLine(path, "{\"presents\":1}\n"); + AgcDriver::TraceFile::AppendLine(path, "{\"presents\":2}\n"); + std::ifstream input(path, std::ios::binary); + const std::string written((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + input.close(); + std::filesystem::remove(path); + Check(written == "{\"presents\":1}\n{\"presents\":2}\n", "summary lines were not appended"); +} + +} + +int main() { + try { + WindowByFrames(); + WindowBySeconds(); + WindowRejects(); + BufferCap(); + FoldShortSlices(); + EscapeStrings(); + EventsShape(); + SummaryFields(); + GpuClock(); + GzipFile(); + AppendLines(); + std::cout << "performance trace 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/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 5c63947898..490ba22a37 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -50,8 +51,10 @@ std::uint32_t low(const void* pointer) { return static_cast(reint std::uint32_t high(const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); } void execute(AgcDriver::QueueState& state, const std::vector& packet) { + AgcDriver::DriverDetail::RegisterStateKey(state); AgcDriver::Pm4::Validate(packet, 0); AgcDriver::Pm4::Execute(packet, state); + check(AgcDriver::DriverDetail::RegisterStateKey(state) == AgcDriver::DriverDetail::RegisterStateKey(state, false, false), "PM4 changed registers without updating their fingerprint"); } void testCatalog() { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 938b001028..dd0b50ba6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -27,9 +27,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" #include "SampleLod_spv.h" +#include "BufferUpdate_spv.h" #include "SampleArray_spv.h" #include "Cover_vert_spv.h" #include "Cover_frag_spv.h" +#include "VertexRange_vert_spv.h" +#include "VertexRangeProducer_frag_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include #ifdef _WIN32 #ifndef NOMINMAX @@ -141,6 +145,9 @@ public: selectedRank = rank; } Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + VkPhysicalDeviceProperties selected{}; + physicalProperties(context.physical, &selected); + std::cout << "Vulkan device: " << selected.deviceName << '\n'; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -303,6 +310,240 @@ private: using Kind = Recorder::ReadKind; +void copiedDirectOrderingTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + context.nullDescriptors = device.NullDescriptors(); + if (context.hostImportAlignment == 0 || !context.nullDescriptors || !AgcDriver::GuestMemory::WriteWatched()) { + std::cout << "Host imports, null descriptors or write watching unavailable: copied/direct ordering test skipped\n"; + return; + } + recorder.Sync(); + const auto bytes = static_cast(std::max(131072, context.hostImportAlignment)); + auto* block = static_cast(AllocateWatched(bytes, bytes)); + Require(block != nullptr, "allocate copied/direct ordering memory"); + const auto address = reinterpret_cast(block); + std::vector> writers; + context.copiedWriters = &writers; + struct Release { + const Context& context; + Recorder& recorder; + std::uint32_t* block; + std::size_t bytes; + std::vector>& writers; + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkShaderModule module = VK_NULL_HANDLE; + std::shared_ptr layoutOwner; + ~Release() { + recorder.Sync(); + writers.clear(); + DrawCopiedWriters()->clear(); + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (module) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + static_cast(HostImportFor(context, reinterpret_cast(block), bytes)); + recorder.Sync(); + ReleaseWatched(block, bytes); + } + } release{context, recorder, block, bytes, writers}; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + std::memset(block, 0, bytes); + Require(HostImportFor(context, address, bytes) != nullptr, "import copied/direct ordering memory"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = 0; + binding.count = 1; + binding.guestDescriptor = {static_cast(address), static_cast(address >> 32u) & 0xffffu, 1024, 0x31000000u}; + binding.bufferWritten = {true}; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto direct = std::make_shared(context, compute); + release.layoutOwner = direct; + Require(!direct->HasCopiedWrites(), "ordering consumer did not bind directly"); + const auto setLayout = direct->Layout(); + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(std::uint32_t)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &release.layout), "ordering pipeline layout"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(BUFFER_UPDATE_SPV); + moduleInfo.pCode = BUFFER_UPDATE_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &release.module), "ordering shader module"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, release.module, "main", nullptr}; + pipelineInfo.layout = release.layout; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &release.pipeline), "ordering pipeline"); + const auto update = [&](const std::shared_ptr& resources, std::uint32_t delta, std::uint32_t count = 1) { + const auto commands = recorder.Commands(); + recorder.Keep(resources); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.pipeline); + resources->Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, release.layout); + context.Function("vkCmdPushConstants")(commands, release.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(delta), &delta); + context.Function("vkCmdDispatch")(commands, count, 1, 1); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_READ_BIT); + resources->MarkGpuWrites(recorder); + }; + for (int mode = 0; mode < 3; ++mode) { + *block = 10; + auto copiedContext = context; + copiedContext.hostImportAlignment = 0; + copiedContext.dmaBufImport = false; + auto copied = std::make_shared(copiedContext, compute); + Require(copied->HasCopiedWrites(), "ordering producer did not take the copied path"); + auto& pending = mode == 2 ? *DrawCopiedWriters() : writers; + pending.push_back(copied); + update(copied, 1); + recorder.OnComplete([copied, &pending] { + copied->WriteBackBuffers(); + std::erase(pending, copied); + }); + Require(!recorder.Idle() && *block == 10, "copied producer was completed before the consumer test"); + if (mode == 0) direct = std::make_shared(context, compute); + else Require(direct->Revalidate(std::span(&compute, 1)), "the imported template could not be revalidated"); + update(direct, 2); + recorder.Sync(); + Require(*block == 13, mode == 0 ? "fresh direct consumer ran before copied write-back" : "reused direct consumer ran before copied write-back"); + } + *block = 20; + block[1024] = 30; + auto copiedContext = context; + copiedContext.hostImportAlignment = 0; + copiedContext.dmaBufImport = false; + auto copied = std::make_shared(copiedContext, compute); + writers.push_back(copied); + update(copied, 1); + recorder.OnComplete([copied, &writers] { + copied->WriteBackBuffers(); + std::erase(writers, copied); + }); + auto separateProgram = program; + separateProgram.bindings[0].guestDescriptor[0] += 4096; + const CompiledShader separateCompute{ShaderRecompiler::ShaderStage::Compute, &separateProgram, 0}; + auto separate = std::make_shared(context, separateCompute); + Require(!writers.empty() && *block == 20, "a disjoint consumer waited for copied writes"); + update(separate, 2); + recorder.Sync(); + Require(*block == 21 && block[1024] == 32, "disjoint copied and direct writes changed each other"); + const std::uint32_t label = 40; + static_cast(recorder.Commands()); + recorder.AfterCompletions(address, std::as_bytes(std::span(&label, 1)), 1000, 0, false); + Require(direct->Revalidate(std::span(&compute, 1)), "template reuse after a CPU completion label failed"); + update(direct, 2); + recorder.Sync(); + Require(*block == 42, "direct consumer preceded an overlapping CPU completion label"); + update(direct, 1); + const auto submissions = recorder.Submissions(); + Require(direct->Revalidate(std::span(&compute, 1)) && recorder.Submissions() == submissions, "ordinary direct work incurred a CPU write-back wait"); + update(direct, 2); + recorder.Sync(); + Require(*block == 45, "direct consumers lost GPU-ordered updates"); + { + TextureDetiler detiler(context); + auto imageContext = context; + imageContext.detiler = &detiler; + TextureCache textureCache(imageContext); + imageContext.textureCache = &textureCache; + Buffer readback(context, 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + struct Clear { + const Context& context; + Recorder& recorder; + ~Clear() { recorder.Sync(); ClearCachedTextures(context.device); } + } clear{context, recorder}; + ShaderRecompiler::RecompileResult imageProgram; + ShaderRecompiler::DescriptorBinding imageBinding; + imageBinding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + imageBinding.role = ShaderRecompiler::DescriptorRole::GuestImages; + imageBinding.descriptorSet = 0; + imageBinding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + imageBinding.count = 1; + imageBinding.imageSamplers = {0}; + imageBinding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + imageBinding.guestDescriptor = {static_cast(address >> 8), static_cast(address >> 40) | (56u << 20) | (3u << 30), 15u | (63u << 14), 0x91b00fac, 0, 0, 0, 0}; + imageProgram.bindings.push_back(imageBinding); + const CompiledShader imageCompute{ShaderRecompiler::ShaderStage::Compute, &imageProgram, 0}; + auto sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + for (int reuse = 0; reuse < 2; ++reuse) { + auto producer = std::make_shared(copiedContext, compute); + writers.push_back(producer); + update(producer, 1); + recorder.OnComplete([producer, &writers] { + producer->WriteBackBuffers(); + std::erase(writers, producer); + }); + if (reuse == 0) sampled = std::make_shared(imageContext, imageCompute); + else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "sampled image template did not revalidate"); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "sampled image did not use imported texture storage"); + const auto commands = recorder.Commands(); + recorder.Keep(sampled); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + std::uint32_t value = 0; + std::memcpy(&value, readback.Bytes().data(), sizeof(value)); + Require(value == static_cast(46 + reuse), "cached sampled image preceded a copied producer"); + } + sampled.reset(); + ClearCachedTextures(context.device); + const auto keys = address + 65536; + const auto keyBytes = DccKeyBytes(65536); + imageProgram.bindings[0].guestDescriptor[6] = (1u << 21) | (static_cast((keys >> 8) & 0xffu) << 24); + imageProgram.bindings[0].guestDescriptor[7] = static_cast(keys >> 16); + auto keyProgram = program; + keyProgram.bindings[0].guestDescriptor = {static_cast(keys), static_cast(keys >> 32u) & 0xffffu, static_cast(keyBytes), 0x31000000u}; + const CompiledShader keyCompute{ShaderRecompiler::ShaderStage::Compute, &keyProgram, 0}; + for (int reuse = 0; reuse < 2; ++reuse) { + std::memset(reinterpret_cast(keys), 0, keyBytes); + AgcDriver::GuestMemory::MarkWritten(keys, keyBytes); + sampled = std::make_shared(imageContext, imageCompute); + recorder.Sync(); + auto producer = std::make_shared(copiedContext, keyCompute); + writers.push_back(producer); + update(producer, 0x40404040, static_cast(keyBytes / 4)); + recorder.OnComplete([producer, &writers] { + producer->WriteBackBuffers(); + std::erase(writers, producer); + }); + if (reuse == 0) sampled = std::make_shared(imageContext, imageCompute); + else Require(sampled->Revalidate(std::span(&imageCompute, 1)), "metadata-only producer invalidated the sampled template"); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "metadata texture is not resident"); + const auto commands = recorder.Commands(); + recorder.Keep(sampled); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + std::uint32_t value = 0; + std::memcpy(&value, readback.Bytes().data(), sizeof(value)); + Require(value == 0xff000000, "cached sampled image ignored copied compression-metadata writes"); + } + } + std::cout << "Copied producers precede fresh and reused direct consumers\n"; +} + void readTrackingTests(const Device& device, Recorder& recorder) { Require(Recorder::ReadTracking(), "read tracking is off (APS5_COPY_READ_TRACKING=0 set?)"); Require(recorder.Idle() && !recorder.PendingReadOverlaps(0x10000, 16), "a fresh recorder reports a read"); @@ -948,6 +1189,128 @@ void remappedImportTests(const Device& device) { HostImportFor(context, address, bytes); } +void hostImportRetirementTests(const Device& device) { + const auto& context = device.GetContext(); + constexpr std::size_t UnitBytes = 65536; + constexpr std::size_t AllocationBytes = 4 * UnitBytes; + constexpr std::size_t CopyBytes = 256; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(UnitBytes, AllocationBytes); +#endif + Require(block != nullptr, "cannot allocate the host import retirement block"); + const auto release = [](void* pointer) { +#ifdef _WIN32 + VirtualFree(pointer, 0, MEM_RELEASE); +#else + std::free(pointer); +#endif + }; + std::unique_ptr allocation(block, release); + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + Recorder recorder(context); + recorder.Activate(); + Buffer readback(context, CopyBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::array registered{}; + const auto cleanup = [&] { + if (!gpu.owns_lock()) gpu.lock(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < registered.size(); ++index) { + if (!registered[index]) continue; + mutation.Remove(memory + index * UnitBytes); + registered[index] = false; + } + SetHostImportWindow(0); + }; + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(memory + index * UnitBytes, UnitBytes, true, true, true); + registered[index] = true; + }; + const auto mapped = [&](std::size_t index) { + GuestAllocations::Mapped result; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address + index * UnitBytes) result.push_back(range); + } + Require(result.size() == 1, "host import retirement lost its registered range"); + return result; + }; + bool deferredEveryRetirement = true; + bool preservedEveryRecorder = true; + try { + const std::array shapes{"same-base growth", "earlier-base overlap", "predecessor split"}; + for (std::size_t shape = 0; shape < shapes.size(); ++shape) { + SetHostImportWindow(0); + Require(ImportMappedRanges(context, {}, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "host import retirement could not adopt the device"); + const std::size_t first = shape == 1 ? 1 : 0; + add(first); + if (shape == 2) add(1); + for (std::size_t index = 0; index < AllocationBytes; ++index) memory[index] = static_cast((index * 37u + shape * 19u + 11u) & 0xffu); + const auto initial = mapped(first); + const auto initialGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool fresh = ImportMappedRanges(context, initial, initialGeneration, false); + const bool unchanged = ImportMappedRanges(context, initial, initialGeneration, false); + gpu.lock(); + Require(fresh && unchanged, "host import retirement blocked a fresh or unchanged fast import"); + const auto sourceAddress = address + (shape == 2 ? UnitBytes : first * UnitBytes); + const auto* imported = HostImportFor(context, sourceAddress, CopyBytes); + Require(imported != nullptr, "host import retirement could not import its source"); + const auto source = imported->buffer; + const auto sourceOffset = sourceAddress - imported->base; + std::array expected{}; + std::memcpy(expected.data(), reinterpret_cast(sourceAddress), CopyBytes); + recorder.Commands(); + recorder.Submit(); + device.WaitQueue(); + Require(!recorder.Recording() && recorder.InFlightBatches() == 1, "host import retirement requires a submitted batch without an open batch"); + std::size_t changed = 0; + if (shape == 0) { + changed = 1; + add(changed); + } else if (shape == 1) { + add(changed); + } else { + SetHostImportWindow(UnitBytes); + add(3); + changed = 1; + } + const auto ranges = mapped(changed); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool deferred = !ImportMappedRanges(context, ranges, generation, false); + gpu.lock(); + const bool preserved = !recorder.Recording() && recorder.InFlightBatches() == 1; + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, source, sourceOffset, readback.Handle(), 0, CopyBytes); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + Require(ImportMappedRanges(context, ranges, generation, true), "host import retirement refused its retry under the GPU lock"); + Require(HostImportFor(context, sourceAddress, CopyBytes) != nullptr, "host import retirement lost the replacement import"); + recorder.Sync(); + readback.Invalidate(); + Require(std::equal(expected.begin(), expected.end(), readback.Bytes().begin()), "host import retirement changed bytes copied from the retired import"); + std::cout << "host import retirement: " << shapes[shape] << " GPU readback verified; deferred=" << deferred << "; recorder preserved=" << preserved << '\n'; + deferredEveryRetirement = deferredEveryRetirement && deferred; + preservedEveryRecorder = preservedEveryRecorder && preserved; + cleanup(); + } + } catch (...) { + cleanup(); + throw; + } + Require(deferredEveryRetirement, "unlocked mapping retired an import instead of requesting the GPU lock"); + Require(preservedEveryRecorder, "unlocked mapping changed recorder batches"); + std::cout << "Host import retirement locking and GPU readback tests passed\n"; +} + void movedMetadataTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -2482,6 +2845,103 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) recorder.Sync(); } +bool writtenBufferOrderTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: written buffer order not tested\n"; + return false; + } + constexpr std::uint32_t side = 256; + constexpr std::size_t bytes = side * side * 4; + void* block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: written buffer order not tested\n"; + return false; + } + std::memset(block, 0x55, 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{base, block, address}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the written buffer order block refused: written buffer order not tested\n"; + return false; + } + std::cout << "written buffer order: host imports are " << (AgcDriver::GuestMemory::Watched(address, bytes) ? "watched" : "compared") << "\n"; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == bytes, "the written buffer order surface has an unexpected size"); + const auto settle = [&] { + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkClearColorValue red{{1.0f, 0.0f, 0.0f, 1.0f}}; + { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &red, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + } + constexpr std::size_t written = 40800; + GuestBufferMemory memory(context); + memory.AcquireRegistered(); + memory.AddWritable(address, written, false, false); + memory.Upload(true); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(address, written, adjustment); + Require(adjustment == 0, "the written buffer view is not aligned to its element"); + { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdFillBuffer")(commands, view.buffer, view.offset, written, 0xa5a5a5a5u); + 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); + } + memory.RecordCopyBacks(recorder); + memory.MarkDirectWrites(); + settle(); + StorageTexture::FlushAllPending("written buffer order test"); + settle(); + std::vector read(bytes); + AgcDriver::GuestMemory::Read(address, read); + Require(std::all_of(read.begin(), read.begin() + written, [](std::byte value) { return value == std::byte{0xa5}; }), "storage results pending under a buffer a later use wrote were stored over the buffer's contents"); + for (std::size_t i = written; i < bytes; ++i) Require(std::to_integer(read[i]) == (i % 4 == 1 || i % 4 == 2 ? 0 : 255), "the storage results outside the written buffer did not reach guest memory"); + } + recorder.Sync(); + return true; +} + void hostImportUnmapTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -3666,9 +4126,7 @@ void importWindowTests(const Device& device, Recorder& recorder) { void* block; ~Release() { ReleaseWatched(block, bytes); } } release{block}; - const auto probe = ProbeImportWriteProtection(base); - Require(probe.failure == nullptr, "(w) the import probe failed"); - const bool importWrites = probe.writtenAtImport != 0; + Require(ProbeImportWriteProtection(base).failure == nullptr, "(w) the import probe failed"); const auto decided = PrepareImportWatch(base); if (decided == ImportWatch::Unwatch) { std::cout << "host imports are compared, not watched: the import window not tested\n"; @@ -3737,16 +4195,11 @@ void importWindowTests(const Device& device, Recorder& recorder) { } Require(!import->unwatched && Watched(address, bytes), "(w) an import under the watch decision left the watch"); CollectWritesUncached(address, surfaceBytes); - if (importWrites) { - Require(!UnchangedSince(address, surfaceBytes, cached), "(w) a cache over the pages the import reported sees no change"); - Require(image->Generation() != 0 && !UnchangedSince(address, surfaceBytes, image->Generation()), "(w) the storage image took its generation after the import window"); - std::array changed{}; - const std::array generations{cached, cached, cached, cached}; - Require(ChangedBlocks(address, surfaceBytes, generations, changed) && std::all_of(changed.begin(), changed.end(), [](std::uint8_t value) { return value == BlockMaybeWritten; }), "(w) the pages the import reported read as written by the CPU"); - Require(!WrittenSince(address, surfaceBytes, cached), "(w) the import window reads as a CPU store"); - } else { - std::cout << "the import reports no pages here: only the pending results of the import window checked\n"; - } + Require(UnchangedSince(address, surfaceBytes, cached), "(w) an import that stored nothing reads as a change to a cache taken before it"); + Require(image->Generation() != 0 && UnchangedSince(address, surfaceBytes, image->Generation()), "(w) an import that stored nothing reads as a change to the pending storage image"); + std::array changed{}; + const std::array generations{cached, cached, cached, cached}; + Require(ChangedBlocks(address, surfaceBytes, generations, changed) && std::all_of(changed.begin(), changed.end(), [](std::uint8_t value) { return value == BlockUnchanged; }), "(w) the pages the import resolved read as written"); const auto after = CollectWritesUncached(address, surfaceBytes); Require(after != 0 && UnchangedSince(address, surfaceBytes, after), "(w) the imported range is not watched again after the import window"); image->Refresh(); @@ -4602,6 +5055,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 { @@ -4628,6 +5087,8 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } std::array texels{}; refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read"); + refused([&] { AgcDriver::GuestMemory::ReadCommitted(address + 256u, texels); }, "a CPU read", "a committed CPU read"); + refused([&] { AgcDriver::GuestMemory::ReadCommittedUnsynced(address + 256u, texels); }, "a CPU read", "a committed CPU read without the flush hook"); refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; @@ -4656,8 +5117,88 @@ 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()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + 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); + clearTargets(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_2_BIT) != 0) { + target.extent = {128, 64}; + target.surfaceExtent = target.extent; + target.samples = 2; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the 2x tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + 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); + clearTargets(); + } + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_8_BIT) != 0) { + target.extent = {64, 32}; + target.surfaceExtent = target.extent; + target.samples = 8; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the 8x tiled multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + 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); + 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"; } @@ -5026,6 +5567,165 @@ void atomicViewTests(const Device& device, Recorder& recorder) { } } +bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool imported) { + if (!device.Graphics() || (imported && device.GetContext().hostImportAlignment == 0)) return false; + constexpr std::size_t unit = 65536; + constexpr std::size_t bytes = unit * 3; + void* watched = AllocateWatched(bytes, unit); + std::vector fallback(watched == nullptr ? bytes + unit : 0); + auto* block = watched != nullptr ? static_cast(watched) : reinterpret_cast((reinterpret_cast(fallback.data()) + unit - 1) & ~(unit - 1)); + struct Release { + void* watched; + ~Release() { if (watched != nullptr) ReleaseWatched(watched, bytes); } + } release{watched}; + std::fill_n(block, bytes, std::byte{0}); + const auto address = reinterpret_cast(block); + auto context = device.GetContext(); + context.recorder = &recorder; + if (!imported) { + context.hostImportAlignment = 0; + context.dmaBufImport = false; + } + TextureDetiler detiler(context); + context.detiler = &detiler; + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Cleanup { + const Context& context; + Recorder& recorder; + void* block; + ~Cleanup() { + recorder.Sync(); + WaitForOptimizedPipelines(context.device); + SharedResourceCache().Clear(); + ClearCachedTextures(context.device); + recorder.Sync(); + ClearCachedPipelines(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } cleanup{context, recorder, block}; + if (imported && HostImportFor(context, address, bytes) == nullptr) return false; + const auto shader = [](auto& words, std::uint64_t variant) { + ShaderRecompiler::RecompileResult program; + program.spirv = std::vector(std::begin(words), std::end(words)); + program.variantId = variant; + return program; + }; + auto producerVertex = shader(COVER_VERT_SPV, 101); + auto producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102); + auto vertex = shader(VERTEXRANGE_VERT_SPV, 103); + auto pixel = shader(COVER_FRAG_SPV, 104); + const std::array producerShaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &producerVertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &producerPixel, 0}}; + const std::array shaders{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}; + const auto stateFor = [](std::uint64_t base, VkFormat format, std::uint32_t elementBytes) { + State state{}; + state.stages.path = ShaderPath::Vertex; + state.color = {base, {64, 64}, format, unit, 0xe4u, ColorTileMode::RenderTarget, elementBytes}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {64, 64}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, 64, 64, -64, 0, 1}; + state.scissor = {{0, 0}, {64, 64}}; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + return state; + }; + const auto producer = stateFor(address + unit, VK_FORMAT_R32G32B32A32_SFLOAT, 16); + const auto consumer = stateFor(address, VK_FORMAT_R8G8B8A8_UNORM, 4); + const AgcDriver::Pm4::DrawParameters triangle{0, 3, 0, 1, 0, false}; + const auto produce = [&] { + Draw(context, producer, triangle, producerShaders); + WaitForOptimizedPipelines(context.device); + const std::array range{std::pair{address + unit, address + 2 * unit}}; + Require(StorageTexture::AnyPendingOverlaps(range), "the color producer did not leave GPU results pending"); + }; + const auto attributeFor = [](std::uint64_t base, std::uint32_t stride, std::uint32_t fetchIndex) { + return ShaderRecompiler::VertexAttribute{0, 4, {{static_cast(base), static_cast(base >> 32u) | (stride << 16u), static_cast(bytes / 16), 77u << 12u}}, fetchIndex}; + }; + const auto check = [&](std::uint32_t edge) { + std::vector texels(64 * 64 * 4); + ReadColorTarget(consumer.color, texels); + for (std::size_t y = 0; y < 64; ++y) { + for (std::size_t x = 0; x < 64; ++x) { + const auto expected = x < edge ? std::byte{0} : std::byte{255}; + for (std::size_t channel = 0; channel < 4; ++channel) Require(texels[(y * 64 + x) * 4 + channel] == expected, "the draw did not consume the GPU-produced vertex offset"); + } + } + }; + const auto reject = [&](auto action, const char* expected) { + try { action(); } + catch (const std::runtime_error& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, error.what()); + return; + } + throw std::runtime_error("an actual render-target alias was accepted"); + }; + for (const std::uint32_t fetchIndex : {0u, 1u}) { + producerPixel = shader(VERTEXRANGEPRODUCER_FRAG_SPV, 102); + vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)}; + auto draw = triangle; + if (fetchIndex == 0) draw.firstVertex = unit / 16; + else draw.firstInstance = unit / 16; + produce(); + std::shared_ptr recipe; + Draw(context, consumer, draw, shaders, {}, &recipe); + WaitForOptimizedPipelines(context.device); + const auto read = recorder.DescribePendingRead(address + unit, 16); + Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput), "the vertex regression used the wrong copied/imported path"); + check(16); + Require(recipe != nullptr, "the range regression did not create a draw recipe"); + std::vector cleared(unit); + AgcDriver::GuestMemory::Write(address, cleared); + for (std::size_t offset = 5; offset < producerPixel.spirv.size();) { + const auto count = producerPixel.spirv[offset] >> 16u; + Require(count != 0, "invalid producer SPIR-V"); + if ((producerPixel.spirv[offset] & 0xffffu) == 43u && count == 4 && producerPixel.spirv[offset + 3] == 0x3f000000u) producerPixel.spirv[offset + 3] = 0x3f400000u; + offset += count; + } + producerPixel.variantId = 105; + produce(); + const auto hit = DrawWithRecipe(context, consumer, draw, shaders, {}, *recipe); + WaitForOptimizedPipelines(context.device); + Require(hit.recorded, "the vertex range regression did not reuse its draw recipe"); + check(24); + vertex.vertexAttributes = {attributeFor(address + unit, 16, fetchIndex)}; + Draw(context, consumer, triangle, shaders); + WaitForOptimizedPipelines(context.device); + check(24); + vertex.vertexAttributes = {attributeFor(address, 16, fetchIndex)}; + auto indirect = draw; + indirect.indirect.emplace(); + reject([&] { Draw(context, consumer, indirect, shaders); }, "vertex buffer aliases the render target"); + if (fetchIndex == 0) { + const std::array indices{0, 1, 2}; + AgcDriver::GuestMemory::Write(address + 2 * unit, std::as_bytes(std::span(indices))); + auto indexed = draw; + indexed.indexed = true; + indexed.indexSize = 2; + indexed.indexAddress = address + 2 * unit; + produce(); + Draw(context, consumer, indexed, shaders); + WaitForOptimizedPipelines(context.device); + check(24); + indexed.indexAddress = address; + reject([&] { Draw(context, consumer, indexed, shaders); }, "index buffer aliases the render target"); + } + reject([&] { Draw(context, consumer, triangle, shaders); }, "vertex buffer aliases the render target"); + vertex.vertexAttributes = {attributeFor(address, 0, fetchIndex)}; + reject([&] { Draw(context, consumer, draw, shaders); }, "vertex buffer aliases the render target"); + } + std::cout << "Vertex range and pending color producer tests passed (" << (imported ? "imported" : "copied") << ")\n"; + return true; +} + void pipelineCacheTests(const Device& device, bool benchmark) { std::thread establishThreadedProcess([] {}); establishThreadedProcess.join(); @@ -5234,6 +5934,21 @@ void keysFillTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void integerClearTests(const Device& device) { + const auto& context = device.GetContext(); + std::size_t tested = 0; + for (const std::uint32_t format : {5u, 11u, 20u}) { + if (!StorageFormatAvailable(context, format)) continue; + ++tested; + Require(StorageClearAvailable(context, format, DccKeys::Clear0000), "an integer storage format refused the 0000 clear"); + for (const auto keys : {DccKeys::Clear0001, DccKeys::Clear1111, DccKeys::ClearRegister, DccKeys::Uncompressed}) { + Require(!StorageClearAvailable(context, format, keys), "an integer storage format took a clear code other than 0000"); + } + } + Require(tested == 3, "an integer storage format has no storage form"); + std::cout << "integer storage clears: " << tested << " formats ok\n"; +} + void denormalClearTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -5396,6 +6111,13 @@ void pipelineLibraryTests(const Device& device) { state.samples = 1; state.colors.front().samples = 1; Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); + state.depth = DepthTarget{0, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + static_cast(lookup()); + Require(counters().built == std::array{2, 3, 5, 5}, "a depth target shared the shader libraries built with no depth format"); + state.depth.reset(); + state.depthTest = false; if ((context.sampleLocationSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { state.samples = 4; state.colors.front().samples = 4; @@ -5436,6 +6158,148 @@ 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 { std::unique_ptr created; @@ -5447,6 +6311,15 @@ int main(int argc, char** argv) { return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { + const auto alignment = device.GetContext().hostImportAlignment; + if (alignment == 0 || alignment > 65536) { + std::cout << "skipped, host import retirement requires host imports aligned within 64 KiB; alignment=" << alignment << '\n'; + return 77; + } + hostImportRetirementTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; @@ -5461,9 +6334,25 @@ 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(); + if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") { + copiedDirectOrderingTests(device, 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; @@ -5477,6 +6366,10 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { + integerClearTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--settled-baseline-only") { settledBaselineTests(device, recorder); return 0; @@ -5491,6 +6384,11 @@ int main(int argc, char** argv) { std::cout << "Completion label storage and ordering tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--written-buffer-order-only") { + if (!writtenBufferOrderTests(device, recorder)) return 77; + std::cout << "written buffer order tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--host-import-unmap-only") { gpu.unlock(); hostImportUnmapTests(device, recorder); @@ -5534,6 +6432,10 @@ int main(int argc, char** argv) { return 0; } + if (argc == 2 && (std::string_view(argv[1]) == "--vertex-target-range-copied" || std::string_view(argv[1]) == "--vertex-target-range-imported")) { + return vertexTargetRangeTests(device, recorder, std::string_view(argv[1]) == "--vertex-target-range-imported") ? 0 : 77; + } + readTrackingTests(device, recorder); writeSettledTests(device, recorder); completionCountTests(device, recorder); @@ -5567,10 +6469,12 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + integerClearTests(device); denormalClearTests(device, recorder); unitShadowTests(device, recorder); storageRefreshTests(device, recorder, false); storageRefreshTests(device, recorder, true); + static_cast(writtenBufferOrderTests(device, recorder)); writeBackPaddingTests(device, recorder, false); writeBackPaddingTests(device, recorder, true); singlePassTests(device, recorder, false); @@ -5591,6 +6495,7 @@ int main(int argc, char** argv) { cmaskPassTests(device, recorder); pendingKeyStoreTests(device, recorder); sampleDumpTests(device, recorder); + copiedDirectOrderingTests(device, recorder); gpu.unlock(); hostImportUnmapTests(device, recorder); drawSnapshotPatchTests(device); diff --git a/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp new file mode 100644 index 0000000000..448d3455a2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RegisterKey.cpp @@ -0,0 +1,140 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver; +using namespace AgcDriver::DriverDetail; + +namespace { + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t baselineRegisterStateKey(const QueueState& queue, bool allUserWords = false) { + std::uint64_t key = 0xcbf29ce484222325ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 0x100000001b3ull; + }; + const auto userEnd = [&](std::uint32_t base) { + if (allUserWords) return base + 32u; + const auto resources = queue.shader.find(base - 1); + if (resources == queue.shader.end()) return base; + const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + return base + std::min(count, 32u); + }; + const std::array, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}}; + const auto unread = [&](std::uint32_t offset) { + return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; }); + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + mix((static_cast(range.bank) << 32u) | range.first); + const auto end = range.first + range.count; + const bool shader = range.bank == Graphics::RegisterBank::Shader; + for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) { + if (shader && unread(it->first)) continue; + mix(it->first); + mix(it->second); + } + } + return key; +} + +void testFingerprints() { + static_assert(!std::is_convertible_v); + std::mt19937 random(42); + QueueState queue; + auto reference = baselineRegisterStateKey(queue); + auto tracked = RegisterStateKey(queue); + const auto compare = [&] { + const auto expected = baselineRegisterStateKey(queue); + const auto actual = RegisterStateKey(queue); + check(actual == RegisterStateKey(queue, false, false), "incremental fingerprint missed a mutation"); + check(RegisterStateKey(queue, true) == RegisterStateKey(queue, true, false), "all-user fingerprint missed a mutation"); + check((expected == reference) == (actual == tracked), "exact key changed its dependency set"); + reference = expected; + tracked = actual; + }; + for (const auto& range : Graphics::DrawKeyRegisters) { + auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig; + for (auto offset = range.first; offset < range.first + range.count; ++offset) { + bank.erase(offset); compare(); + bank.emplace(offset, 0); compare(); + bank.insert_or_assign(offset, 0); compare(); + bank[offset] = random(); compare(); + bank.at(offset) ^= 1; compare(); + } + } + for (std::uint32_t i = 0; i < 30000; ++i) { + auto& bank = i % 3 == 0 ? queue.context : i % 3 == 1 ? queue.shader : queue.userConfig; + const auto offset = random() % 0x500; + switch (i % 11) { + case 0: bank.erase(offset); break; + case 1: bank.emplace(offset, random()); break; + case 2: bank[offset] |= random(); break; + case 3: bank[offset] &= random(); break; + case 6: bank[offset] += random(); break; + case 4: { + auto retained = bank[offset]; + bank.insert_or_assign(0x1000, random()); + compare(); + retained = random(); + break; + } + case 5: { + auto copy = bank; + copy.insert_or_assign(offset, random()); + compare(); + bank = copy; + break; + } + default: bank.insert_or_assign(offset, random()); break; + } + compare(); + } + queue.shader[0x8b] = 0; compare(); + queue.shader[0x8c] = random(); compare(); + queue.shader[0x8b] = 2; compare(); + queue.shader[0x8b] = 1u << 27u; compare(); + queue.shader.erase(0x8b); compare(); + queue.savedContext = queue.context; + queue.ClearContext(); compare(); + queue.context = *queue.savedContext; compare(); + auto moved = std::move(queue.context); + check(queue.context.empty(), "moved register bank retained its population"); + queue.context.emplace(0x200, 1); compare(); + queue.context = std::move(moved); compare(); + queue.context = queue.context; compare(); + queue.context.clear(); compare(); + queue.context.emplace(0x200, 0); compare(); + queue = QueueState{}; compare(); + Registers registers{{0, 7}, {64, 9}}; + auto low = std::make_shared(std::vector{1}); + auto high = std::make_shared(std::vector{0, 1}); + const auto lowHash = registers.Fingerprint(low); + check(registers.Fingerprint(high) != lowHash && registers.Fingerprint(low) == lowHash, "projection switch reused another mask's hash"); + registers[64] = 8; + check(registers.Fingerprint(low) == lowHash && registers.Fingerprint(high) == registers.RecomputeFingerprint(*high), "projection switch lost a masked write"); +} + + +} + +int main() { + try { + testFingerprints(); + std::puts("Register fingerprint dependencies and mutations passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 063d4a4993..6ab1458817 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -246,6 +246,7 @@ CompiledVariant sampleVariant() { info.info.hasBitwiseXor = true; info.info.usesDma = true; info.info.usesFaultBuffer = true; + info.info.rasterPrimitive = true; info.bindings.pushDataStartDword = 2; info.bindings.memoryOffsetDword = 1; info.bindings.memoryOffsetCount = 5; @@ -414,6 +415,7 @@ void verifyKeySensitivity() { changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; }); changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); + changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 57596b5d62..6e412161ac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -12,6 +12,7 @@ #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#include "IntermediateRepresentation/IrProgram.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -610,6 +611,89 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } +void verifyGuardedDescriptorSources() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 0u, 0u, 0u, 0u, 0u}; + const std::array sampler{0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + 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"}; + RecompileRequest request{}; + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 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; + + fill(invalid, 0u); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "guarded sources: the image and its sampler were not planned"); + require(!plan->controlFlow.empty(), "guarded sources: the branch around the image was not planned"); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: the skipped T# was not left null"); + } + fill(invalid, 1u); + { + AgcDriver::ShaderMemory memory({}); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); + } + fill(valid, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "guarded sources: the T# behind a taken branch was not read"); + } + fill(valid, 0u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "guarded sources: a valid T# behind a skipped branch was read"); + } + + const std::vector storeCode{0xf4000400u, 0xfa000050u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f08u, 0x00010004u, 0xbf820005u, 0xf40c0100u, 0xfa000000u, 0xbf8cc07fu, 0xf0200f10u, 0x00010004u, 0xbf810000u}; + auto storeRequest = request; + storeRequest.shader = {ShaderStage::Compute, 0x23000u, storeCode, 0, {}}; + const auto compileStore = [&](std::uint32_t mode) { + fill(valid, mode); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(storeRequest); + storeRequest.context.memory = memory.Regions(); + return Recompile(storeRequest, *capture); + }; + const auto storePlan = GetResourcePlan(storeRequest); + require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#"); + static_cast(compileStore(1u)); + expectFailure([&] { static_cast(compileStore(0u)); }, "incompatible with the static runtime image interface", "guarded images: a store of the wrong dimension behind a taken branch was accepted"); +} + // A compute program sampling a T# loaded from a table buffer at a runtime key (a bindless image // table): mode M enumerates the keys from the material records, mode T binds the whole table. void verifyBindlessTable() { @@ -1577,6 +1661,22 @@ void verifyMeshConfiguration() { request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}}; const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request)); require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization"); + { + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + ShaderVertexStageInfo info{}; + info.paClVsOutCntl = 0x0040000fu; + vertex.context.vertex = info; + const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(vertex)); + require(back.request.context.vertex.has_value() && back.request.context.vertex->paClVsOutCntl == 0x0040000fu, "PA_CL_VS_OUT_CNTL was lost in serialization"); + std::vector clipped; + RecompileCacheKey::Build(vertex, clipped); + vertex.context.vertex->paClVsOutCntl = 0; + std::vector unclipped; + RecompileCacheKey::Build(vertex, unclipped); + require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key"); + } std::vector key; RecompileCacheKey::Build(request, key); const auto first = key; @@ -1586,6 +1686,23 @@ void verifyMeshConfiguration() { other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; RecompileCacheKey::Build(other, key); require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); + 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}, {}}; + 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"); + auto grouped = hull; + grouped.graphics->tessellation->patchesPerGroup = 62u; + for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { + hull.shader.stage = stage; + grouped.shader.stage = stage; + RecompileCacheKey::Build(hull, key); + const auto patchKey = key; + 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"); + } const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -1724,15 +1841,141 @@ void verifyPixelRequestSerialization() { minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; minimal.context.waveSize = 64; minimal.context.pixel = ShaderPixelStageInfo{}; + minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; + minimal.context.pixel->layerExported = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRIAAAA=", "new requests did not use serialization version 18"); + 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(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 < 19u; ++bytes) { - expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage or stencil reference export flag was accepted"); + 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"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRMAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRgAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } + static constexpr std::string_view version18Request{ + "NVNQQRIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAB0gQAAAE="}; + const auto version18 = serializer.Deserialize(version18Request); + require(version18.request.context.pixel.has_value() && version18.request.context.pixel->rasterPrimitive == RasterPrimitive::Triangle, "a version 18 request read a primitive class"); + require(version18.request.target.registrationModules && version18.request.target.registrationWordLimit == 1234u && version18.request.target.vertexSubgroups, "a version 18 request trailer was misread"); +} + +void verifyPositionExportComponents() { + constexpr std::uint32_t none = 0xffffffffu; + struct Case { + std::uint32_t control; + std::array clip; + std::array cull; + }; + const std::array cases{{ + {0x00400000u, {none, none, none, none}, {none, none, none, none}}, + {0x00400001u, {0u, none, none, none}, {none, none, none, none}}, + {0x00400002u, {none, 0u, none, none}, {none, none, none, none}}, + {0x00400005u, {0u, none, 1u, none}, {none, none, none, none}}, + {0x00400100u, {none, none, none, none}, {0u, none, none, none}}, + {0x00400400u, {none, none, none, none}, {none, none, 0u, none}}, + }}; + for (const auto& test : cases) { + for (std::uint32_t component = 0; component < 4; ++component) { + const auto output = ShaderRecompiler::DecodePositionExportComponent(test.control, 1u, component); + require(output.clipDistance == test.clip[component] && output.cullDistance == test.cull[component], "PA_CL_VS_OUT_CNTL decoded a position component to the wrong clip or cull distance"); + } + } +} + +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, "invalid base64 symbol"); + 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; +} + +void verifyVertexInfoVersion19() { + using namespace ShaderRecompiler; + const std::array code{0xbf810000u}; + const RequestSerializer serializer; + RecompileRequest vertex{}; + vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}}; + vertex.context.waveSize = 64; + vertex.context.vertex = ShaderVertexStageInfo{}; + const auto unmarked = serializer.Serialize(vertex); + vertex.context.vertex->paClVsOutCntl = 0x89abcdefu; + const auto marked = serializer.Serialize(vertex); + const auto plain = decodeBase64(unmarked); + const auto flagged = decodeBase64(marked); + require(plain.size() == flagged.size(), "PA_CL_VS_OUT_CNTL changed the request size"); + std::size_t offset = 0; + 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"); + 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"); + auto legacy = plain; + legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); + for (const auto version : {16u, 17u, 18u, 19u}) { + legacy[4] = static_cast(version); + const auto replay = serializer.Deserialize(encodeBase64(legacy)); + require(replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0u, "a legacy vertex info read a PA_CL_VS_OUT_CNTL word"); + require(serializer.Serialize(replay.request) == unmarked, "a legacy vertex info misread the fields after it"); + } + RecompileRequest pixel{}; + pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}}; + pixel.context.waveSize = 64; + pixel.context.pixel = ShaderPixelStageInfo{}; + pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle; + const auto encodedPixel = serializer.Serialize(pixel); + auto exportedPixel = pixel; + exportedPixel.context.pixel->layerExported = true; + const auto exportedBytes = decodeBase64(serializer.Serialize(exportedPixel)); + auto legacyPixel = decodeBase64(encodedPixel); + 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)); + 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"); + require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 to 22 request misread the fields after its pixel primitive class"); + } } void verifyLegacyPixelRequests() { @@ -1821,12 +2064,13 @@ void verifyLegacyPixelRequests() { require(!pixel.dualSourceBlend, "a legacy request enabled dual-source blending"); require(!pixel.alphaToCoverage, "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"); require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread"); require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader"); require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned"); require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned"); - require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread"); + require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread"); const auto upgraded = serializer.Deserialize(serializer.Serialize(request)); require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping"); } @@ -2182,6 +2426,105 @@ void verifyInt64AtomicCapabilities() { require(declares(compile(imageAtomic, spv::CapabilityInt64ImageEXT), spv::CapabilityInt64ImageEXT), "64-bit atomics: an image_atomic_swap on a 32_32 image does not declare Int64ImageEXT"); } +void verifySpecializedBufferAtomicBindings() { + using namespace ShaderRecompiler; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + alignas(256) static std::array buffer{}; + const auto bufferAddress = static_cast(reinterpret_cast(buffer.data())); + const std::array userData{ + 0u, 0u, 0u, 0u, + static_cast(bufferAddress), static_cast((bufferAddress >> 32u) & 0xffffu), 256u, 0x31016facu, + 0u, 0u, 0u, 0u, + 0u, 0u, 0u, 0u}; + const std::vector code{0xe0f05000u, 0x80012803u, 0xbf810000u}; + const auto compile = [&](std::span supported) { + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x58800u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = supported; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request); + }; + const auto result = compile(capabilities); + std::size_t tables = 0; + std::size_t faults = 0; + std::size_t atomicElements = 0; + for (const auto& binding : result.bindings) { + if (binding.role == DescriptorRole::BdaPagetable) ++tables; + if (binding.role == DescriptorRole::FaultBuffer) ++faults; + atomicElements += static_cast(std::count(binding.bufferAtomic.begin(), binding.bufferAtomic.end(), true)); + } + 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; + std::uint32_t faultFunction = 0; + std::size_t faultCalls = 0; + bool physical = false; + bool int64 = false; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "specialized buffer atomic: truncated SPIR-V instruction"); + if (opcode == spv::OpCapability && words[cursor + 1] == spv::CapabilityPhysicalStorageBufferAddresses) physical = true; + if (opcode == spv::OpCapability && words[cursor + 1] == spv::CapabilityInt64) int64 = true; + if (opcode == spv::OpMemoryModel && words[cursor + 1] != spv::AddressingModelLogical) physical = true; + if (opcode == spv::OpName && std::string_view(reinterpret_cast(&words[cursor + 2])) == "record_bda_fault") faultFunction = words[cursor + 1]; + if (opcode == spv::OpFunctionCall && faultFunction != 0 && words[cursor + 3] == faultFunction) ++faultCalls; + cursor += count; + } + require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); + require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); + require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault"); + const std::array shaderOnly{spv::CapabilityShader}; + expectFailure([&] { static_cast(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64"); +} + +void verifyCoherentGds() { + using namespace ShaderRecompiler; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::vector code{0x34000082u, 0xbefc03ffu, 0x0000ffffu, 0xd8da0000u, 0x01000000u, 0xbf8cc07fu, 0xd8360080u, 0x00000100u, 0xbf810000u}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x5c000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + const auto words = Recompile(request).spirv; + std::uint32_t gds = 0u; + std::map> decorations; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "coherent GDS: truncated SPIR-V instruction"); + if (opcode == spv::OpName && std::string_view(reinterpret_cast(&words[cursor + 2])) == "gds") gds = words[cursor + 1]; + if (opcode == spv::OpDecorate) decorations[words[cursor + 1]].push_back(words[cursor + 2]); + cursor += count; + } + require(gds != 0u, "coherent GDS: a ds_read_b32 and ds_write_b32 on GDS declare no gds buffer"); + require(std::ranges::find(decorations[gds], static_cast(spv::DecorationCoherent)) != decorations[gds].end(), "coherent GDS: the gds buffer is not Coherent"); +} + void verifyCoherentBufferAccesses() { using namespace ShaderRecompiler; alignas(256) static std::array input{}; @@ -2361,9 +2704,9 @@ void verifyUnnormalizedSamplers() { 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"); - for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u}) { + for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u, 0xf0880f08u}) { const auto result = recompile(program(mimg), userData(unnormalized)); - require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b or image_sample_cl was not flagged"); + require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b, image_sample_cl or image_sample_d was not flagged"); auditSpirv(result.spirv.Words()); } @@ -2373,7 +2716,6 @@ void verifyUnnormalizedSamplers() { reject(0xf0bc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz was accepted"); reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); - reject(0xf0880f08u, userData(unnormalized), "unnormalized guest sampler is used by a sample with derivatives, which is not implemented", "unnormalized samplers: image_sample_d was accepted"); reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); @@ -2448,6 +2790,14 @@ void verifyUnnormalizedSamplers() { require(!masks.empty() && std::ranges::all_of(masks, [](std::uint32_t mask) { return (mask & spv::ImageOperandsOffsetMask) != 0u; }), "unnormalized texel offsets: a normalized computed offset lost its Offset operand"); } + const auto gradient = program(0xf0880f08u); + const auto gradientAddress = nextAddress += 0x1000u; + const auto gradientUnnormalized = recompile(gradient, userData(unnormalized), gradientAddress); + const auto gradientNormalized = recompile(gradient, userData(normalized), gradientAddress); + require(proven(gradientNormalized, false) && gradientNormalized.variantId == gradientUnnormalized.variantId && gradientNormalized.PipelineVariantId() != gradientUnnormalized.PipelineVariantId(), "unnormalized samplers: a normalized image_sample_d did not get its own specialization of the shared artifact"); + require(sampleMasks(gradientNormalized.spirv.Words()) == std::vector{spv::ImageOperandsGradMask}, "unnormalized samplers: a normalized image_sample_d lost its derivatives"); + require(sampleMasks(gradientUnnormalized.spirv.Words()) == std::vector{spv::ImageOperandsLodMask}, "unnormalized samplers: an unnormalized image_sample_d kept its derivatives"); + const std::array pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; const auto pixelData = userData(unnormalized); RecompileRequest pixel{}; @@ -2461,7 +2811,23 @@ void verifyUnnormalizedSamplers() { pixel.target.subgroupSize = 64; pixel.layout.pushConstantSizeBytes = 128; pixel.useCache = false; - expectFailure([&] { static_cast(Recompile(pixel)); }, "unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented", "unnormalized samplers: a pixel image_sample was accepted"); + const auto implicitSamples = [](const std::vector& words) { + std::size_t samples = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && count <= words.size() - cursor, "unnormalized samplers: truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod) ++samples; + cursor += count; + } + return samples; + }; + const auto pixelUnnormalized = Recompile(pixel); + require(proven(pixelUnnormalized, true), "unnormalized samplers: a pixel image_sample through an unnormalized S# was not flagged"); + auditSpirv(pixelUnnormalized.spirv.Words()); + const auto normalizedPixelData = userData(normalized); + pixel.context.userData = normalizedPixelData; + const auto pixelNormalized = Recompile(pixel); + require(proven(pixelNormalized, false) && implicitSamples(pixelNormalized.spirv.Words()) == 1u, "unnormalized samplers: a pixel image_sample through a normalized S# is not an implicit-LOD sample"); } void verifyUnusedUnnormalizedSampler() { @@ -2677,11 +3043,16 @@ void verifyBdaReadFallbackFunctions() { std::map definitions; const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); const auto span = std::string("read_bda_span") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : ""); + const auto loader = std::string("load_bda_span") + (coherent ? "_coherent" : ""); std::string function; std::size_t compareExchanges = 0; std::size_t mainLookups = 0; std::size_t mainProbes = 0; std::size_t spanCalls = 0; + std::size_t spanLoads = 0; + std::size_t spanLoaderCalls = 0; + std::size_t loaderLoads = 0; + std::size_t loaderCalls = 0; std::array readerLoads{}; for (std::size_t cursor = 5; cursor < five.size();) { const auto length = five[cursor] >> 16u; @@ -2690,7 +3061,7 @@ void verifyBdaReadFallbackFunctions() { if (op == spv::OpName) names[five[cursor + 1]] = reinterpret_cast(&five[cursor + 2]); if (op == spv::OpFunction) { function = names[five[cursor + 2]]; - if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes") || function.starts_with("read_bda_span")) { + if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes") || function.starts_with("read_bda_span") || function.starts_with("load_bda_span")) { require((five[cursor + 3] & spv::FunctionControlDontInlineMask) != 0u, "BDA read functions: a fault, byte read or span read function may be inlined"); ++definitions[function]; } @@ -2702,10 +3073,16 @@ void verifyBdaReadFallbackFunctions() { if (callee == "probe_bda_pointer" || callee.starts_with("read_bda_dword_bytes")) ++mainProbes; if (callee == span) ++spanCalls; } + if ((op == spv::OpLoad || op == spv::OpAtomicLoad) && function == span) ++spanLoads; + if (op == spv::OpFunctionCall && function == span && names[five[cursor + 3]] == loader) ++spanLoaderCalls; + 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]; 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"); + require(definitions[loader] == 1u && loaderLoads == 4u && loaderCalls == 0u, "BDA read functions: the span load function does not hold the four in-place loads and nothing else"); + require(spanLoads == 0u && spanLoaderCalls == 1u, "BDA read functions: a span read function loads in place itself"); 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"); @@ -3097,6 +3474,7 @@ void verifyGuardedNullPointers() { const auto tableAddress = static_cast(reinterpret_cast(table.data())); vertexRoot[2] = static_cast(tableAddress); vertexRoot[3] = static_cast(tableAddress >> 32u); + vertexRoot[0] = 1u; AgcDriver::ShaderMemory mappedVertexMemory({}); const auto mappedVertex = mappedVertexMemory.Capture(vertex.request); require(mappedVertex->snapshot.srtPoison.empty() && mappedVertex->snapshot.buffers.size() == 1u && mappedVertex->snapshot.buffers[0].dwords[0] == table[4], "guarded pointer: a vertex V# behind a mapped pointer was poisoned or not read"); @@ -3188,6 +3566,7 @@ int main(int argc, char** argv) { verifyOutOfRangeConstantReads(); verifySignedMinimum(); verifyPureFlatSlots(); + verifyGuardedDescriptorSources(); verifyBindlessTable(); verifyDescriptorPhis(); verifyLaneSelectedDescriptors(); @@ -3199,6 +3578,8 @@ int main(int argc, char** argv) { verifyMeshConfiguration(); verifyPixelInputs(); verifyPixelRequestSerialization(); + verifyVertexInfoVersion19(); + verifyPositionExportComponents(); verifyLegacyPixelRequests(); verifyPixelExportReplay(); verifyPixelParameterSlots(); @@ -3206,6 +3587,7 @@ int main(int argc, char** argv) { verifyComputedTexelOffsets(); verifyShaderClockScopes(); verifyInt64AtomicCapabilities(); + verifySpecializedBufferAtomicBindings(); verifyCoherentBufferAccesses(); verifyUnnormalizedSamplers(); verifyUnusedUnnormalizedSampler(); @@ -3217,6 +3599,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); verifySrgbDepthCompareModes(); + verifyCoherentGds(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, @@ -3330,6 +3713,9 @@ int main(int argc, char** argv) { RecompileCacheKey::Build(uncached, plainKey); RecompileCacheKey::Build(registrationModules, registrationKey); require(plainKey != registrationKey, "registration modules did not change the cache key"); + auto float16Denormals = uncached; + float16Denormals.target.float16DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float16Denormals)).request.target.float16DenormPreserve, "f16 denormal preservation was lost in serialization"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp index cd11b37971..f1fe06c422 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp @@ -144,28 +144,42 @@ void partialCollect() { for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - address); return offsets; }; - const std::vector all{0, page, page * 2, page * 3}; + const std::vector none; collect(alias, 0, mapping.size, 4, true); data[page * 3] = std::byte{92}; - require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe missed a shared write"); - require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe consumed a shared write"); - require(collect(mapping.data, 0, page, 4, true) == std::vector{0}, "prefix reset missed a shared write"); - require(collect(alias, 0, mapping.size, 4, false) == all, "prefix reset consumed an alias write"); + require(collect(mapping.data, 0, page * 3, 4, false) == none, "a shared write was reported for the pages next to it"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 3}, "probe missed a shared write"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 3}, "probe consumed a shared write"); + require(collect(mapping.data, 0, page, 4, true) == none, "prefix reset reported an unwritten page"); + require(collect(alias, 0, mapping.size, 4, false) == std::vector{page * 3}, "prefix reset consumed an alias write"); require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "prefix reset consumed a neighboring write"); require(collect(mapping.data, page * 3, page, 4, false).empty(), "reset subpage remained dirty"); - require(collect(mapping.data, page, page * 2, 4, true) == std::vector{page, page * 2}, "partial resets consumed intervening writes"); require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "collected subpages remained dirty"); - require(collect(alias, 0, mapping.size, 4, true) == all, "collecting one view consumed an alias write"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page * 3}, "collecting one view consumed an alias write"); static_cast(alias)[page] = std::byte{93}; - require(collect(mapping.data, page * 2, page, 4, true) == std::vector{page * 2}, "alias write did not invalidate the other view"); - require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{0, page, page * 3}, "full collect after an interior reset did not preserve pending subpages"); + require(collect(mapping.data, page * 2, page, 4, true) == none, "an alias write was reported for another page"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{page}, "alias write did not invalidate the other view"); require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "full collect left pending subpages"); - static_cast(alias)[page * 3] = std::byte{94}; + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page}, "alias write was consumed for its own view"); + for (std::size_t at = 0; at < mapping.size; at += page) static_cast(alias)[at + 8] = std::byte{99}; + require(collect(mapping.data, page, page * 2, 4, true) == std::vector{page, page * 2}, "partial resets consumed intervening writes"); + require(collect(mapping.data, page, page * 2, 4, false).empty(), "interior reset left its subpages dirty"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{0, page * 3}, "full collect after an interior reset did not preserve pending subpages"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{0, page, page * 2, page * 3}, "an interior reset consumed the alias's writes"); + static_cast(alias)[0] = std::byte{94}; + static_cast(alias)[page * 2] = std::byte{95}; + static_cast(alias)[page * 3] = std::byte{96}; require(collect(mapping.data, 0, mapping.size, 1, true) == std::vector{0}, "limited collect returned the wrong prefix"); - require(collect(mapping.data, 0, mapping.size, 4, false) == all, "limited collect consumed unreported writes"); - require(collect(mapping.data, page, page * 3, 3, true) == std::vector{page, page * 2, page * 3}, "limited collect continuation lost writes"); - require(collect(mapping.data, 0, page, 1, true) == std::vector{0}, "limited collect consumed its unreset prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false) == std::vector{page * 2, page * 3}, "limited collect consumed unreported writes"); + require(collect(mapping.data, page, page * 3, 1, true) == std::vector{page * 2}, "limited collect continuation lost writes"); + require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "limited collect consumed a write after its last report"); require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "limited collection did not settle"); + require(collect(alias, 0, mapping.size, 4, true) == std::vector{0, page * 2, page * 3}, "limited collection consumed the alias's writes"); + data[page] = std::byte{97}; + data[page * 2] = std::byte{98}; + require(collect(alias, 0, mapping.size, 4, true) == std::vector{page, page * 2}, "writes to two pages were not reported apart"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{page, page * 2}, "writes to two pages were not reported for their own view"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty() && collect(alias, 0, mapping.size, 4, false).empty(), "page tracking did not settle"); require(sceKernelMunmap(alias, mapping.size) == 0, "partial collect alias unmap failed"); #endif std::cout << "Partial shared write collection tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp new file mode 100644 index 0000000000..dda775da27 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvBranchTargets.cpp @@ -0,0 +1,73 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector recompile(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu, 0u, 0u, 0u, 0u, 7u, 9u, 0x10002000u, 0u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; + 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 = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return std::move(result.spirv); +} + +void requireDistinctTargets(const char* name, std::span code) { + const auto words = recompile(code); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + require(count != 0 && cursor + count <= words.size(), std::string(name) + ": truncated SPIR-V instruction"); + if ((words[cursor] & 0xffffu) == spv::OpBranchConditional) { + require(words[cursor + 2] != words[cursor + 3], std::string(name) + ": OpBranchConditional with the same true and false label, invalid in SPIR-V 1.6"); + } + cursor += count; + } +} + +} + +int main() { + try { + constexpr std::array zeroOffsetBranch{0xbf840000u, 0xbf810000u}; + requireDistinctTargets("s_cbranch_scc0 to the next instruction", zeroOffsetBranch); + constexpr std::array unalignedStore{0x7e020281u, 0xe0700000u, 0x80020100u, 0xbf810000u}; + requireDistinctTargets("store through a descriptor with an unaligned base", unalignedStore); + } catch (const std::exception& error) { + std::fprintf(stderr, "SpirvBranchTargets: %s\n", error.what()); + return 1; + } + std::puts("SpirvBranchTargets: no conditional branch names one label twice"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 98353c1399..cf6e8a94fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -1,8 +1,10 @@ #include "SpirvBackend/SpirvModule.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" +#include #include #include #include +#include #include #include @@ -223,7 +225,68 @@ void testStructExtractsAreNotShuffled() { } } +std::vector foldedSwitchModule(std::uint32_t deadSum, std::array localSize) { + const auto op = [](std::uint32_t count, spv::Op code) { return (count << 16u) | static_cast(code); }; + return { + spv::MagicNumber, 0x00010300u, 0u, 40u, 0u, + op(2, spv::OpCapability), spv::CapabilityShader, + op(3, spv::OpMemoryModel), spv::AddressingModelLogical, spv::MemoryModelGLSL450, + op(6, spv::OpEntryPoint), spv::ExecutionModelGLCompute, 11u, 0x6e69616du, 0u, 10u, + op(6, spv::OpExecutionMode), 11u, spv::ExecutionModeLocalSize, localSize[0], localSize[1], localSize[2], + op(2, spv::OpTypeVoid), 1u, + op(4, spv::OpTypeInt), 2u, 32u, 0u, + op(4, spv::OpTypePointer), 4u, spv::StorageClassPrivate, 2u, + op(3, spv::OpTypeFunction), 5u, 1u, + op(4, spv::OpConstant), 2u, 6u, 2u, + op(4, spv::OpConstant), 2u, 7u, 10u, + op(4, spv::OpConstant), 2u, 8u, 20u, + op(4, spv::OpVariable), 4u, 10u, spv::StorageClassPrivate, + op(5, spv::OpFunction), 1u, 11u, 0u, 5u, + op(2, spv::OpLabel), 12u, + op(5, spv::OpIAdd), 2u, deadSum, 6u, 7u, + op(5, spv::OpIMul), 2u, 14u, deadSum, 7u, + op(3, spv::OpSelectionMerge), 20u, spv::SelectionControlMaskNone, + op(7, spv::OpSwitch), 6u, 15u, 1u, 16u, 2u, 17u, + op(2, spv::OpLabel), 15u, + op(3, spv::OpStore), 10u, 7u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 16u, + op(3, spv::OpStore), 10u, 8u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 17u, + op(3, spv::OpStore), 10u, 14u, + op(2, spv::OpBranch), 20u, + op(2, spv::OpLabel), 20u, + op(1, spv::OpReturn), + op(1, spv::OpFunctionEnd), + }; +} + +void checkFoldedSwitch(const std::vector& words, std::uint32_t deadSum, const char* what) { + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + const auto counts = opcodeCounts(specialized); + std::set constants; + bool storesFolded = false; + for (std::size_t cursor = 5; cursor < specialized.size(); cursor += specialized[cursor] >> 16u) { + if ((specialized[cursor] >> 16u) == 0u) break; + const auto opcode = specialized[cursor] & 0xffffu; + if (opcode == spv::OpConstant) constants.insert(specialized[cursor + 2u]); + if (opcode == spv::OpStore && specialized[cursor + 2u] == 14u) storesFolded = true; + } + const std::string context = std::string(" (") + what + ")"; + check(!counts.contains(spv::OpUnreachable), ("blocks the folded switch left unreachable were kept" + context).c_str()); + check(counts.at(spv::OpLabel) == 3u, ("the specialized function does not have exactly its three live blocks" + context).c_str()); + check(storesFolded && constants.contains(14u), ("the folded product the live case stores is missing" + context).c_str()); + check(!constants.contains(deadSum), ("a folded constant without uses was kept" + context).c_str()); +} + +void testFoldingLeavesNoDeadBlocksOrConstants() { + checkFoldedSwitch(foldedSwitchModule(13u, {1u, 1u, 1u}), 13u, "no literal names a dead id"); + checkFoldedSwitch(foldedSwitchModule(32u, {15u, 16u, 1u}), 32u, "the OpTypeInt width and the LocalSize literals equal the dead ids"); +} + int main() { + testFoldingLeavesNoDeadBlocksOrConstants(); testStructExtractsAreNotShuffled(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index c511be056e..b9c3911406 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -15,8 +15,10 @@ namespace { int failures = 0; constexpr std::uint32_t Vulkan11 = 0x00401000u; +constexpr std::uint32_t Vulkan13 = 0x00403000u; constexpr std::uint32_t Spirv11 = 0x00010100u; constexpr std::uint32_t Spirv13 = 0x00010300u; +constexpr std::uint32_t Spirv16 = 0x00010600u; void check(bool condition, const std::string& message) { if (!condition) { @@ -241,6 +243,52 @@ OpFunctionEnd } } +void testSpecializedDeadContinue() { + for (const bool folded : {false, true}) { + const std::string source = std::string(R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 8 1 1 +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%false = OpConstantFalse %bool +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +OpBranch %header +%header = OpLabel +%count = OpPhi %uint %zero %entry %next %continue +OpLoopMerge %exit %continue None +OpBranch %body +%body = OpLabel +OpStore %output %count +)") + (folded ? "OpBranchConditional %false %continue %exit\n" : "OpBranch %exit\n") + R"(%continue = OpLabel +%next = OpIAdd %uint %count %one +OpBranch %header +%exit = OpLabel +OpReturn +OpFunctionEnd +)"; + const std::string label = folded ? "loop exit on a constant condition" : "loop with an unreachable continue target"; + try { + const auto words = assemble(source); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, Vulkan11, Spirv13, false, false)); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specialized, spv::OpLoopMerge) == 1u, label + ": specialization lost the loop"); + check(opcodeCount(specialized, spv::OpIAdd) == 0u, label + ": specialization retained the dead continue block"); + check(ShaderRecompiler::SpecializeSpirv(specialized) == specialized, label + ": specialization is not stable"); + } catch (const std::exception& error) { + check(false, label + ": " + error.what()); + } + } +} + void testInvalidInputIsRejected() { const auto valid = assemble(moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly)); expectRejected(valid, 0x00400000u, Spirv13, "SPIR-V 1.3 with Vulkan 1.0"); @@ -277,14 +325,69 @@ void testInvalidInputIsRejected() { } } +void testSpecializedSelectionExitSpirv16() { + const std::string source = R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index %output +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%input = OpTypePointer Input %uint +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%true = OpConstantTrue %bool +%index = OpVariable %input Input +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +%condition = OpIEqual %bool %lane %zero +OpSelectionMerge %exit None +OpBranchConditional %true %body %dead +%body = OpLabel +OpBranchConditional %condition %exit %nested +%nested = OpLabel +OpSelectionMerge %join None +OpBranchConditional %condition %write %join +%write = OpLabel +OpStore %output %lane +OpBranch %join +%join = OpLabel +%sum = OpIAdd %uint %lane %lane +OpBranch %exit +%dead = OpLabel +OpBranch %exit +%exit = OpLabel +%value = OpPhi %uint %zero %body %sum %join %zero %dead +OpStore %output %value +OpReturn +OpFunctionEnd +)"; + try { + auto words = assemble(source); + words[1] = Spirv16; + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(words, Vulkan13, Spirv16, false, false)); + const auto specialized = ShaderRecompiler::SpecializeSpirv(words); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specialized, Vulkan13, Spirv16, false, false)); + check(ShaderRecompiler::SpecializeSpirv(specialized) == specialized, "SPIR-V 1.6 selection exit specialization is not stable"); + } catch (const std::exception& error) { + check(false, std::string("specialized SPIR-V 1.6 selection exit: ") + error.what()); + } +} + } int main() { const char* mode = std::getenv("APS5_SPIRV_OPT"); const bool optimizationEnabled = mode == nullptr || std::string(mode) != "none"; testNarrowConversions(optimizationEnabled); + testSpecializedSelectionExitSpirv16(); testInvalidInputIsRejected(); testSpecializedSelectionExit(); + testSpecializedDeadContinue(); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/StackWalk.cpp b/core/libs/prx/libSceAgcDriver/tests/StackWalk.cpp new file mode 100644 index 0000000000..aa8370e367 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/StackWalk.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceAgcDriver/Execution/include/StackWalk.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::CollapsedStacks; +using AgcDriver::DwarfFrames; +using Byte = DwarfFrames::Byte; +using Word = DwarfFrames::Word; +using Step = DwarfFrames::Step; + +constexpr std::size_t FunctionOffset = 0x100; +constexpr std::size_t FunctionSize = 0x40; + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Section { +public: + explicit Section(Byte returnColumn) : bytes(0x200, 0) { + const std::size_t cie = append32(0); + append32(0); + append({1, 'z', 'R', 0, 1, 0x78, returnColumn, 1, 0x1b, 0x0c, 7, 8, static_cast(0x80 | returnColumn), 1}); + patch32(cie, static_cast(cursor - cie - 4)); + const std::size_t fde = append32(0); + append32(static_cast(cursor - cie)); + append32(static_cast(Function() - Address(cursor))); + append32(FunctionSize); + append({0, 0x41, 0x0e, 0x10, 0x86, 0x02, 0x43, 0x0d, 0x06, 0, 0}); + patch32(fde, static_cast(cursor - fde - 4)); + end = cursor; + append32(0); + } + + Word Function() const { + return Address(FunctionOffset); + } + + DwarfFrames Frames() const { + return DwarfFrames(Address(0), Address(bytes.size()), bytes.data(), bytes.data() + end + 4); + } + +private: + Word Address(std::size_t offset) const { + return reinterpret_cast(bytes.data() + offset); + } + + std::size_t append32(std::uint32_t value) { + const auto at = cursor; + std::memcpy(bytes.data() + cursor, &value, 4); + cursor += 4; + return at; + } + + void append(std::initializer_list values) { + for (const auto value : values) bytes[cursor++] = value; + } + + void patch32(std::size_t at, std::uint32_t value) { + std::memcpy(bytes.data() + at, &value, 4); + } + + std::vector bytes; + std::size_t cursor = 0; + std::size_t end = 0; +}; + +struct Stack { + std::array words{}; + + Word At(std::size_t index) const { + return reinterpret_cast(&words[index]); + } + + auto Reader() const { + return [this](Word address, Word& value) { + if (address < At(0) || address > At(words.size() - 1)) return false; + std::memcpy(&value, reinterpret_cast(address), sizeof(value)); + return true; + }; + } +}; + +DwarfFrames::Registers Registers(Word pc, Word sp, Word fp) { + DwarfFrames::Registers registers{}; + registers[16] = pc; + registers[7] = sp; + registers[6] = fp; + return registers; +} + +void Prologue() { + const Section section(16); + const auto frames = section.Frames(); + const auto function = section.Function(); + Stack stack; + stack.words[32] = 0x5555; + stack.words[33] = 0x1234; + + auto registers = Registers(function, stack.At(33), 0x9999); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the entry of a function did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x9999, "the entry of a function stepped to the wrong frame"); + + registers = Registers(function + 2, stack.At(32), 0x9999); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped, "the frame after the push did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the frame after the push did not restore the frame pointer"); + + registers = Registers(function + 0x20, stack.At(8), stack.At(32)); + Check(frames.Unwind(registers, false, stack.Reader()) == Step::Stepped, "the body of a function did not step"); + Check(registers[16] == 0x1234 && registers[7] == stack.At(34) && registers[6] == 0x5555, "the body of a function did not step through the frame pointer"); +} + +void Outside() { + const Section section(16); + const auto frames = section.Frames(); + const auto function = section.Function(); + Stack stack; + auto registers = Registers(function + FunctionSize, stack.At(8), 0); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc after the function had a record"); + registers = Registers(function - 1, stack.At(8), 0); + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a pc before the function had a record"); + Check(frames.Covers(function + 0x10) && !frames.Covers(function + FunctionSize), "the record covers the wrong range"); + registers = Registers(function, stack.At(63) + 4, 0); + const auto before = registers; + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Failed && registers == before, "a read outside the stack was not refused"); + Check(DwarfFrames(0, 0, nullptr, nullptr).Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a module without records stepped"); +} + +void ReturnColumn32() { + const Section section(32); + const auto frames = section.Frames(); + Stack stack; + stack.words[10] = 0x4321; + auto registers = Registers(section.Function(), stack.At(10), 0); +#ifdef _WIN32 + Check(frames.Unwind(registers, true, stack.Reader()) == Step::Stepped && registers[16] == 0x4321 && registers[7] == stack.At(11), "the MinGW return column was not read"); +#else + Check(frames.Unwind(registers, true, stack.Reader()) == Step::NoRecord, "a return column above 16 was accepted"); +#endif +} + +void Collapsed() { + CollapsedStacks stacks; + const std::uint64_t full[] = {0x10, 0x20, 0x30}; + const std::uint64_t cut[] = {0x10, 0x40}; + stacks.Add(full, false); + stacks.Add(full, false); + stacks.Add(cut, true); + const auto text = stacks.Format([](std::uint64_t address) { + char name[32]; + std::snprintf(name, sizeof(name), "m+0x%llx", static_cast(address)); + return std::string(name); + }); + Check(text == "m+0x30;m+0x20;m+0x10 2\n[truncated];m+0x40;m+0x10 1\n", "collapsed stacks changed format"); + Check(stacks.Truncated() == 1, "the truncated walk was not counted"); +} + +} + +int main() { + try { + Prologue(); + Outside(); + ReturnColumn32(); + Collapsed(); + std::cout << "stack walk 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/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index ec50e84c9d..21a2806af9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Eq/include/Query.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include #include @@ -211,6 +212,22 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed"); AgcDriverWaitIdle_nid_postfix(); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt"); + struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { + std::atomic ready{0}; + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } + void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } + void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } + }; + const auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; + Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == 1, "flip was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared"); + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); words = {0xc0064900, 0x528, (3u << 29u) | (3u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 0x89abcdefu, 0x01234567u, 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 26ad1c130b..33d1a9f457 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -4,11 +4,13 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include #include #include #include #include #include +#include namespace { @@ -96,6 +98,50 @@ void convertedDccClearTests() { Require(read(20, 0x40) == DccKeys::Clear0001 && read(20, 0x80) == DccKeys::Clear1110, "format 20 must keep its 0001 and 1110 DCC clear codes"); } +void singleDccKeysTests() { + alignas(256) std::array texels{}; + for (std::size_t index = 0; index < texels.size(); ++index) texels[index] = 0x01000000u + static_cast(index); + alignas(64) std::array keys{}; + keys.fill(0x10); + GuestTextureResource resource{}; + resource.baseAddress = reinterpret_cast(texels.data()); + resource.dccAddress = reinterpret_cast(keys.data()); + const auto read = [&](std::uint32_t format) { + resource.format = format; + return TextureClearKeys(resource, sizeof(texels)); + }; + const auto* original = reinterpret_cast(texels.data()); + for (const std::uint32_t format : {1u, 56u, 77u}) { + Require(read(format) == DccKeys::ClearSingle, "comp-to-single keys of format " + std::to_string(format) + " must read as single"); + const auto elementBytes = BytesPerElement(format); + std::vector surface(sizeof(texels)); + ReadTextureSurface(resource, DccKeys::ClearSingle, surface); + bool expanded = true; + for (std::size_t offset = 0; offset < surface.size(); ++offset) expanded = expanded && surface[offset] == original[offset / 256 * 256 + offset % elementBytes]; + Require(expanded, "a read of format " + std::to_string(format) + " under comp-to-single keys must give every 256-byte block its first element"); + } + Require(read(74) == DccKeys::Uncompressed, "comp-to-single keys of a format whose element does not divide 256 bytes must read as stored"); + keys[1] = 0xff; + Require(read(56) == DccKeys::Uncompressed, "comp-to-single keys mixed with uncompressed ones must read as stored"); +} + +void packedFloatDccClearTests() { + for (const bool alphaOnMsb : {false, true}) { + for (const auto keys : {DccKeys::Clear0000, DccKeys::Clear0001, DccKeys::Clear1110, DccKeys::Clear1111}) { + std::array texels{}; + texels.fill(0xdeadbeefu); + Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, keys, alphaOnMsb, std::as_writable_bytes(std::span(texels))), "packed float fast clear was not encoded"); + const std::uint32_t expected = keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 ? 0x781e03c0u : 0u; + for (const auto texel : texels) Require(texel == expected, "packed float DCC clear did not encode RGB zero or one"); + } + } + alignas(64) std::array keys{0xc0, 0xc0, 0xc0, 0xc0}; + GuestTextureResource resource{}; + resource.format = 36; + resource.dccAddress = reinterpret_cast(keys.data()); + Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys"); +} + } void RunTextureFormatTests() { @@ -145,4 +191,6 @@ void RunTextureFormatTests() { for (const std::uint32_t format : {128u, 129u, 130u, 170u, 172u, 174u, 182u}) Require(IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must be an sRGB texture format"); for (const std::uint32_t format : {1u, 14u, 56u, 71u, 169u, 171u, 173u, 181u}) Require(!IsSrgbTextureFormat(format), "guest format " + std::to_string(format) + " must not be an sRGB texture format"); convertedDccClearTests(); + singleDccKeysTests(); + packedFloatDccClearTests(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 7f622c9562..2028dcb2b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -27,6 +27,17 @@ constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; constexpr std::uint32_t MovreldV4V0 = 0x7e088500u; constexpr std::uint32_t MoveV1V5 = 0x7e020305u; constexpr std::uint32_t EndProgram = 0xbf810000u; +constexpr std::uint32_t MoveV2Zero = 0x7e040280u; +constexpr std::uint32_t MoveV3Zero = 0x7e060280u; +constexpr std::uint32_t WritelaneV4 = 0xd7610004u; +constexpr std::uint32_t FromS8Lane0 = 0x00010008u; +constexpr std::uint32_t ReadlaneS8 = 0xd7600008u; +constexpr std::uint32_t FromV4Lane0 = 0x00010104u; +constexpr std::uint32_t ImageSampleLz2d = 0xf09c0f08u; +constexpr std::uint32_t SampleV8V2S0S8 = 0x00400802u; +constexpr std::uint32_t AddV8V1V8 = 0x4a101101u; +constexpr std::uint32_t BufferStoreDword = 0xe0700000u; +constexpr std::uint32_t StoreV8WithS12 = 0x80030800u; struct Shape { std::size_t functionArrays = 0; @@ -39,9 +50,8 @@ void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } -std::vector RecompileProgram(std::span code) { +std::vector RecompileProgram(std::span code, std::span userData) { const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; - const std::array userData{0x10000000u, 0u, 0x40u, 0x31016facu}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; RecompileRequest request{}; request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; @@ -65,6 +75,11 @@ std::vector RecompileProgram(std::span code) return std::vector(result.spirv.Words().begin(), result.spirv.Words().end()); } +std::vector RecompileProgram(std::span code) { + const std::array userData{0x10000000u, 0u, 0x40u, 0x31016facu}; + return RecompileProgram(code, userData); +} + Shape Scan(std::span words) { Shape shape; std::vector> constants; @@ -160,8 +175,8 @@ void RequireWindow(const IrProgram& program, std::uint32_t base, std::uint32_t l stored[index] = true; } else if (instruction->Opcode() == IrOpcode::SetVectorRegister) { const auto index = instruction->Argument(0)->Register().index; - Require(index < base, "stage input escaped the relative window into SSA"); - stored[index] = true; + Require(index < base || !program.Metadata().vectorWindowIndexedWrites, "stage input escaped the relative window into SSA"); + if (index < base) stored[index] = true; } } for (const auto index : initialized) Require(stored[index], "stage input v" + std::to_string(index) + " was not initialized"); @@ -271,6 +286,13 @@ int main(int argc, char** argv) { return shape.functionArrays == 0 && shape.accessChains == 0; }); + passed &= run("a sampler dword spilled into a relatively read register keeps its definition through v_readlane", [] { + const std::array code{MoveM0GroupX, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovrelsV1V4, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, AddV8V1V8, BufferStoreDword, StoreV8WithS12, EndProgram}; + const std::array userData{}; + const auto shape = Scan(RecompileProgram(code, userData)); + return shape.functionArrays == 1; + }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp new file mode 100644 index 0000000000..27a7785a5a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexLayerExport.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Module { + std::vector capabilities; + std::vector extensions; + bool layerOutput = false; + std::uint32_t version = 0; +}; + +Module inspect(const std::vector& words) { + Module module; + if (words.size() > 1) module.version = words[1]; + std::vector layerIds; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) break; + if (opcode == 17u && length == 2) module.capabilities.push_back(words[at + 1]); + if (opcode == 10u && length >= 2) module.extensions.emplace_back(reinterpret_cast(&words[at + 1])); + if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) module.layerOutput = true; + at += length; + } + return module; +} + +bool has(const std::vector& values, std::uint32_t value) { + for (const auto entry : values) { + if (entry == value) return true; + } + return false; +} + +bool named(const std::vector& values, std::string_view value) { + for (const auto entry : values) { + if (entry == value) return true; + } + return false; +} + +Module recompile(std::uint32_t control, bool layerExtension) { + using namespace ShaderRecompiler; + static const std::array code{0xf80000cfu, 0x00000000u, 0xf80008dfu, 0x01010101u, 0xbf810000u}; + static const std::array withExtension{1u, 32u, 5254u}; + static const std::array without{1u, 32u}; + ShaderVertexStageInfo vertex{}; + vertex.paClVsOutCntl = control; + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x30000u, code, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = vertex; + request.target.vulkanVersion = layerExtension ? 0x00401000u : 0x00402000u; + request.target.spirvVersion = layerExtension ? 0x00010300u : 0x00010500u; + request.target.subgroupSize = 64; + if (layerExtension) request.target.supportedCapabilities = withExtension; + else request.target.supportedCapabilities = without; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return inspect(Recompile(request).spirv); +} + +} + +int main() { + try { + constexpr std::uint32_t layer = (1u << 18u) | (1u << 21u); + const auto extension = recompile(layer, true); + if (!extension.layerOutput || !has(extension.capabilities, 5254u) || has(extension.capabilities, 69u) || !named(extension.extensions, "SPV_EXT_shader_viewport_index_layer") || extension.version != 0x00010300u) { + std::fprintf(stderr, "a vertex layer export with VK_EXT_shader_viewport_index_layer: layer %d, version 0x%x\n", extension.layerOutput ? 1 : 0, extension.version); + return 1; + } + const auto core = recompile(layer, false); + if (!core.layerOutput || !has(core.capabilities, 69u) || has(core.capabilities, 5254u) || core.version != 0x00010500u) { + std::fprintf(stderr, "a vertex layer export without the extension: layer %d, version 0x%x\n", core.layerOutput ? 1 : 0, core.version); + return 1; + } + const auto ignored = recompile(0u, true); + if (ignored.layerOutput || has(ignored.capabilities, 5254u)) { + std::fprintf(stderr, "a misc export without USE_VTX_RENDER_TARGET_INDX wrote gl_Layer\n"); + return 1; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "unexpected error: %s\n", error.what()); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 8528450d4e..f839edd79d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" #ifdef _WIN32 @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -23,6 +25,9 @@ namespace { using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; +#ifdef _WIN32 +constexpr wchar_t HostHoleMode[] = L"--host-hole"; +#endif void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -179,6 +184,65 @@ void CheckNewestStamp() { } #ifdef _WIN32 +void CheckDeferredSharedImports() { + constexpr std::size_t bytes = 4 * Block; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, bytes, nullptr); + Require(section != nullptr, "cannot create shared import backing"); + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); + Require(memory != nullptr, "cannot allocate shared import address"); + GuestArena::GuestArenaMap_nid_postfix(memory, bytes, section, 0, PAGE_READWRITE); + const auto base = reinterpret_cast(memory); + const auto protection = [&](std::size_t offset) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query shared import protection"); + return info.Protect; + }; + Require(ImportWatched(base, bytes, [] { return true; }), "fresh shared import failed"); + Require(protection(0) == PAGE_READWRITE && protection(bytes - 1) == PAGE_READWRITE, "fresh import armed shared pages"); + const auto gpu = MarkWritten(base + 4096, 4); + Require(gpu != 0 && protection(0) == PAGE_READONLY && protection(Block) == PAGE_READWRITE, "GPU write did not activate only its block"); + CollectWritesUncached(base, Block); + Require(!WrittenSince(base + 4096, 4, gpu) && !StoredOver(base + 4096, 4, gpu), "first collect overwrote a GPU generation"); + static_cast(memory)[4096] = 0x44; + CollectWritesUncached(base, Block); + Require(WrittenSince(base + 4096, 4, gpu) && StoredOver(base + 4096, 4, gpu), "CPU store after GPU write was missed"); + + const auto second = base + Block; + const auto before = CollectWritesUncached(second, 4096); + Require(MarkWritten(second, 4) != 0, "GPU write next to fresh pages failed"); + Require(UnchangedSinceCollected(second, 4, before), "fresh neighbors were stamped as new CPU writes over a collected page"); + Require(StoredOver(second + 8 * 4096, 4096, before), "fresh neighbor history was lost"); + const std::array generations{before}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(second, Block, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 0, "fresh collection set the block's CPU stamp"); + const auto generation = TrackerGeneration(); + Require(MarkWritten(second, 4) == generation + 1, "an already collected GPU block advanced twice"); + + const auto third = base + 2 * Block; + const auto cached = CollectWritesUncached(third, Block); + Require(ImportWatched(third, Block, [&] { + static_cast(memory)[2 * Block] = 0x55; + return true; + }), "tracked import window failed"); + Require(!UnchangedSince(third, Block, cached) && WrittenSince(third, Block, cached), "a store of new bytes within a tracked import window was missed"); + const auto afterImport = CollectWritesUncached(third, Block); + static_cast(memory)[2 * Block] = 0x66; + Require(ImportWatched(third, Block, [] { return true; }) && WrittenSince(third, Block, afterImport), "pre-import CPU store was not collected"); + + GuestArena::GuestArenaReset_nid_postfix(memory + 3 * Block, 16384); + const auto edge = base + 3 * Block + 16384; + Require(ImportWatched(edge, Block - 16384, [] { return true; }), "unaligned shared import failed"); + const auto edgeGpu = MarkWritten(edge, 4); + Require(edgeGpu != 0, "fresh collection rejected an unmapped block neighbor"); + CollectWritesUncached(edge, Block - 16384); + Require(!WrittenSince(edge, 4, edgeGpu), "edge block collection overwrote GPU results"); + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); + CloseHandle(section); +} + + void CheckPrivateMappingReuse() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(memory); @@ -211,28 +275,49 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } -std::uint64_t ArenaHostHole() { +std::uint64_t ChildHostHole() { std::uintptr_t arena = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); - for (auto cursor = arena; cursor < arena + arenaBytes;) { - MEMORY_BASIC_INFORMATION info{}; - Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); - if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) return cursor; - const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; - Require(next > cursor, "arena enumeration did not advance"); - cursor = next; + return (arena + arenaBytes / 2) & ~(std::uint64_t{Block} - 1); +} + +void CheckHostHoleInChild() { + std::array executable{}; + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length != 0 && length < executable.size(), "cannot name the test executable"); + const std::wstring command = L"\"" + std::wstring(executable.data(), length) + L"\" " + HostHoleMode; + std::vector commandLine(command.begin(), command.end()); + commandLine.push_back(L'\0'); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(executable.data(), commandLine.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &startup, &process) != 0, "cannot create the host-hole child"); + const bool reserved = VirtualAllocEx(process.hProcess, reinterpret_cast(ChildHostHole()), Block, MEM_RESERVE, PAGE_NOACCESS) != nullptr; + const bool resumed = reserved && ResumeThread(process.hThread) != static_cast(-1); + const bool exited = resumed && WaitForSingleObject(process.hProcess, 60000) == WAIT_OBJECT_0; + if (!exited) { + TerminateProcess(process.hProcess, 1); + WaitForSingleObject(process.hProcess, INFINITE); } - return 0; + DWORD exitCode = 1; + const bool reported = GetExitCodeProcess(process.hProcess, &exitCode) != 0; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + Require(reserved, "cannot reserve a host range in the child's arena interval"); + Require(resumed, "cannot resume the host-hole child"); + Require(exited, "the host-hole child did not exit"); + Require(reported && exitCode == 0, "the host-hole child failed"); } void CheckHostHole() { constexpr std::size_t bytes = 64; - const auto hole = ArenaHostHole(); - if (hole == 0) { - std::cout << "host-hole query checks skipped: the arena contains no host hole\n"; - return; - } + const auto hole = ChildHostHole(); + Require(GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(hole, bytes), "the range reserved before the arena is not an arena host region"); const auto before = TrackerGeneration(); Require(!Watched(hole, bytes), "an arena host hole reads as watched"); Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); @@ -388,22 +473,120 @@ void CheckCollectBoundaries() { } } +void CheckImportWindow() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 3 * Block); + const auto synced = CollectWritesUncached(base, 3 * Block); + Require(synced != 0, "the import window block is not collected"); + Require(ImportWatched(base, 3 * Block, [&] { + bytes[0x100] = 0x11; + bytes[Block + 0x2000] = 0x22; + return true; + }), "the import window import failed"); + Require(Watched(base, 3 * Block), "an import window left its range unwatched"); + const std::array generations{synced, synced, synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, 3 * Block, generations, changed, cpu), "the import window block is not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base, Block, synced), "a page an import window resolved without changing its bytes reads as written"); + Require(changed[1] == BlockWritten && WrittenSince(base + Block, Block, synced) && cpu[1] == 1, "a store of new bytes within an import window was missed"); + Require(changed[2] == BlockUnchanged && cpu[2] == 0, "a block the import window did not touch reads as written"); + Require(cpu[0] == 1, "a page an import window resolved no longer counts as possibly touched by the CPU"); + const auto after = CollectWritesUncached(base, 3 * Block); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, 3 * Block)); + Require(!UnchangedSince(base, Block, after), "a store after the import window was missed"); +} + +void ProtectPage(std::uint64_t address, bool readable) { +#ifdef _WIN32 + DWORD previous = 0; + Require(VirtualProtect(reinterpret_cast(address), 4096, readable ? PAGE_READWRITE : PAGE_NOACCESS, &previous) != 0, "cannot change the protection of a test page"); +#else + Require(mprotect(reinterpret_cast(address), 4096, readable ? PROT_READ | PROT_WRITE : PROT_NONE) == 0, "cannot change the protection of a test page"); +#endif + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(static_cast(address), 4096); +} + +void CheckUnwatchedImport() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + auto* bytes = static_cast(memory); + std::memset(memory, 0x11, 3 * Block); + const auto synced = CollectWritesUncached(base, 3 * Block); + Require(synced != 0, "the unwatched import block is not collected"); + const auto imported = base + Block / 2; + const std::size_t importedBytes = Block + Block / 2; + const auto unreadable = base + Block + 0x3000; + ProtectPage(unreadable, false); + bool threw = false; + bool ran = false; + try { + static_cast(ImportWatched(imported, importedBytes, [&] { + ran = true; + return true; + })); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && !ran, "an import that stays watched did not read its range before the import"); + threw = false; + try { + Require(ImportWatched(imported, importedBytes, [&] { + bytes[Block / 2 + 0x100] = 0x22; + ran = true; + return true; + }, false), "an import that leaves the watch failed"); + } catch (const std::runtime_error&) { + threw = true; + } + Require(!threw && ran, "an import that leaves the watch read its range"); + ProtectPage(unreadable, true); + Require(!Watched(imported, importedBytes) && !Watched(imported, 4096) && !Watched(base + Block, Block), "an import that leaves the watch is still watched"); + Require(Watched(base + 2 * Block, Block), "an import that leaves the watch unwatched a block outside its range"); + Require(UnchangedSince(base + 2 * Block, Block, synced), "an import that leaves the watch stamped a block outside its range"); +#ifndef _WIN32 + Require(Watched(base, Block / 2), "an import that leaves the watch unwatched the pages before its range"); + const std::array generations{synced}; + std::array changed{}; + std::array cpu{}; + Require(ChangedBlocks(base, Block / 2, generations, changed, cpu), "the pages before an import that leaves the watch are not tracked"); + Require(changed[0] == BlockUnchanged && UnchangedSince(base, Block / 2, synced) && !WrittenSince(base, Block / 2, synced), "a store to a page that left the watch reads as a store to the watched pages of its block"); + Require(cpu[0] == 1, "a page resolved while an import left the watch no longer counts as possibly touched by the CPU"); + const auto after = CollectWritesUncached(base, Block / 2); + bytes[0x100] = 0x33; + static_cast(CollectWritesUncached(base, Block / 2)); + Require(after != 0 && !UnchangedSince(base, Block / 2, after), "a store beside an import that left the watch was missed"); +#endif +} + int main() { try { if (!WriteWatched()) { std::cout << "no write watching: skipped\n"; return 77; } +#ifdef _WIN32 + if (std::wstring_view(GetCommandLineW()).ends_with(HostHoleMode)) { + CheckHostHole(); + return 0; + } +#endif CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); #ifdef _WIN32 + CheckDeferredSharedImports(); CheckPrivateMappingReuse(); - CheckHostHole(); + CheckHostHoleInChild(); #endif CheckVersionRanges(); CheckCollectBoundaries(); + CheckImportWindow(); + CheckUnwatchedImport(); } 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/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp index 53887c1b4c..6c7664d7bf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -113,12 +113,12 @@ private: std::uint8_t* block = nullptr; }; -std::array ArrayDescriptor(std::uint64_t address) { +std::array ArrayDescriptor(std::uint64_t address, std::uint32_t swizzleMode) { return { static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), - 0xfacu | (TileR64KBX << 20u) | (Type2DArray << 28u), + 0xfacu | (swizzleMode << 20u) | (Type2DArray << 28u), Layers - 1u, 0u, 0u, @@ -146,13 +146,13 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::span co device.WaitIdle(); } -void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer) { +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer, std::uint32_t swizzleMode) { 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); + const auto texture = ArrayDescriptor(surface, swizzleMode); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); Dispatch(device, code, userData); @@ -165,22 +165,23 @@ void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::strin } } -AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice) { +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { AgcDriver::Registers cx; cx[0x318] = static_cast(address >> 8u); cx[0x31b] = slice | (slice << 13u); cx[0x31c] = Color32FloatInfo; cx[0x31d] = 0; cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); - cx[0x3b8] = TiledAttrib3 | (Layers - 1u); + cx[0x3b8] = (TiledAttrib3 & ~(0x1fu << 14u)) | (swizzleMode << 14u) | (collapsed ? 0u : Layers - 1u); cx[0x390] = static_cast(address >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); - Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + if (collapsed) Require(color.arrayAddress == 0 && color.arrayLastSlice == 0, "a collapsed slice view unexpectedly describes a whole array"); + else Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); return color; } -void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice) { +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice, std::uint32_t swizzleMode, bool collapsed) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::size_t at = 0; @@ -207,7 +208,7 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto color = DecodeTarget(surface, slice); + const auto color = DecodeTarget(surface, slice, swizzleMode, collapsed); VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; AgcDriver::Graphics::State state{}; @@ -246,29 +247,31 @@ void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint device.WaitIdle(); } -void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint32_t swizzleMode, bool collapsed = false, std::uint32_t slice = 1) { const auto surface = AddressOf(texels); std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); AgcDriver::GuestMemory::BumpCollectEpoch(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto drawn = std::bit_cast(Drawn); - DrawSlice(device, surface, 1); - const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); - Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); - LoadLayer(device, surface, 1); - RequireOutput(drawn, Initial, "slice 1 read through the array after the draw"); - for (const std::uint32_t other : {0u, 2u, 3u}) { - LoadLayer(device, surface, other); - RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1"); + DrawSlice(device, surface, slice, swizzleMode, collapsed); + const auto pending = StorageTexture::FindPending(collapsed ? surface + slice * LayerBytes : surface, collapsed ? LayerBytes : SurfaceBytes); + if (collapsed) Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2D && pending->Descriptor().baseAddress == surface + slice * LayerBytes, "a collapsed slice did not render into its shifted surface"); + else Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); + LoadLayer(device, surface, slice, swizzleMode); + RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " read through the array after the draw"); + for (std::uint32_t other = 0; other < Layers; ++other) { + if (other == slice) continue; + LoadLayer(device, surface, other, swizzleMode); + RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice " + std::to_string(slice)); } StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); device.WaitIdle(); AgcDriver::Graphics::ClearCachedTextures(device.Device()); AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); AgcDriver::GuestMemory::BumpCollectEpoch(); - LoadLayer(device, surface, 1); - RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory"); + LoadLayer(device, surface, slice, swizzleMode); + RequireOutput(drawn, Initial, "slice " + std::to_string(slice) + " uploaded again from guest memory"); AgcDriver::Graphics::ClearCachedTextures(device.Device()); } @@ -279,7 +282,9 @@ int main() { GuestBlock block; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, block.Data()); + Run(*device, block.Data(), TileR64KBX); + Run(*device, block.Data(), static_cast(AgcDriver::Graphics::ColorTileMode::D64KBX)); + for (std::uint32_t slice = 1; slice < Layers; ++slice) Run(*device, block.Data(), TileR64KBX, true, slice); std::puts("array layer target tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp index c4b7506b90..f587262360 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterCopy.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" #include "VulkanTestDevice.hpp" @@ -109,6 +110,11 @@ int main() { const GuestBlock output; const GuestBlock destination; RecordDispatch(*device, input, output); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "copied writer: no active recorder"); + constexpr std::array labelValue{std::byte{1}, std::byte{0}, std::byte{0}, std::byte{0}}; + recorder->NoteLabel(output.Address(), labelValue, 1, 0); + Require(recorder->AfterRecordedWork([] {}), "copied writer: completion action was not recorded"); AgcDriver::VulkanDevice::CopyOutcome outcome{}; { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index 5a9bf5c21f..eae20eb5a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -1,8 +1,10 @@ #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/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.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" @@ -32,6 +34,16 @@ #include #include +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + namespace { using AgcDriver::Graphics::DccKeys; @@ -66,6 +78,13 @@ alignas(256) constexpr std::array ClearKeysCode{ 0x34020083, 0x7e040280, 0x7e060280, 0xe0741000, 0x80000201, 0xbf810000, }; +alignas(256) constexpr std::array SampleSecondTexelCode{ + 0x34060084, 0x7e2802ff, 0x3c400000, 0x7e2a02ff, 0x3b800000, 0xf09c0f08, 0x00610a14, 0xbf8c3f70, + 0xe0781000, 0x80000a03, 0xbf810000, +}; + +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -211,8 +230,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(AgcDriver::Graphics::StorageImageServesKeys(*image, AddressOf(copied)), "the written image does not serve the copied keys"); Require(image->UploadedKeys() == DccKeys::Uncompressed && image->FilledKeys() == DccKeys::Uncompressed, "the written image still holds the clear it was uploaded under"); const bool stillWatched = AgcDriver::GuestMemory::Watched(surface, SurfaceBytes); - const bool keepsCopy = !stillWatched || AgcDriver::Graphics::WatchedImportsStampPages(); - Require(image->GuestSnapshotValid() == keepsCopy, !keepsCopy ? "a fast-cleared upload of write-watched memory read its guest bytes" : stillWatched ? "a fast-cleared upload of write-watched memory that an import window can stamp kept no guest bytes to compare" : "a fast-cleared upload of untracked memory kept no guest bytes to compare"); + Require(image->GuestSnapshotValid() == !stillWatched, stillWatched ? "a fast-cleared upload of write-watched memory read its guest bytes" : "a fast-cleared upload of untracked memory kept no guest bytes to compare"); const auto pendingVersion = image->Version(); for (int repeat = 0; repeat < 3; ++repeat) { Require(image->Refresh(), "an unchanged pending image was uploaded again"); @@ -309,7 +327,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { const std::array pattern{word, word, word, word}; return device.FillBuffer(AddressOf(keys + offset), bytes, pattern); }; - if (!fill(0, KeyBytes, 0x10)) { + if (!fill(0, KeyBytes, 0x5a)) { std::puts("the key range has no host import: pending key fills are not tested"); return; } @@ -317,7 +335,9 @@ void Run(AgcDriver::VulkanDevice& device, std::uint8_t* block) { std::puts("host imports are compared, not watched: the write-watched block runs as unwatched"); watched = false; } - readPending(DccKeys::Uncompressed, false, "a pending fill of the keys with 0x10, which is no clear code"); + 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"); 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"); Require(fill(0, KeyBytes / 2, 0x40), "the 0001 fill of the first key half was not recorded"); @@ -369,6 +389,175 @@ void RunReusedKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { Require(Count(texels, SurfaceBytes, 0xff) == Threads * 4u, "reused keys: the write-back lost the image's results"); } +void RunSingleKeys(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + const auto surface = AddressOf(texels); + std::vector original(SurfaceBytes / 4); + for (std::size_t index = 0; index < original.size(); ++index) original[index] = 0x01000000u + static_cast(index); + std::memcpy(texels, original.data(), SurfaceBytes); + std::memset(keys, 0x10, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "comp-to-single keys: the first write did not store the keys uncompressed"); + RequireKeys(keys, 0xff, "comp-to-single keys: the keys after the first write"); + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test comp-to-single keys"); + device.WaitIdle(); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::size_t wrong = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + const auto offset = layout.Offset(x, y); + std::uint32_t texel = 0; + std::memcpy(&texel, texels + offset, 4); + const auto expected = y == 0 && x < Threads ? 0xffffffffu : original[offset / 256 * 64]; + if (texel != expected) ++wrong; + } + } + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys: %zu of %u written-back texels are neither the kernel's write nor the first texel of their 256-byte block", wrong, Side * Side); + Require(wrong == 0, message); +} + +void RunSingleKeysSampled(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock, const char* memory) { + auto* texels = block; + auto* keys = block + KeysOffset; + auto* output = reinterpret_cast(outputBlock); + const bool watched = AgcDriver::GuestMemory::Watched(AddressOf(texels), SurfaceBytes); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + const auto first = layout.Offset(1, 0) / 256 * 256; + std::memset(texels, 0x00, SurfaceBytes); + std::memset(texels + first, 0xff, 4); + std::memset(keys, 0x10, KeyBytes); + const auto texture = TextureDescriptor(texels, keys); + const auto results = BufferDescriptor(output, Threads * 16u); + std::vector userData(24, 0u); + std::copy(results.begin(), results.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto sample = [&](std::uint32_t expected, const char* when) { + std::fill(output, output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(device, SampleSecondTexelCode, userData); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys sampled %s from %s memory: component %u of thread %u is 0x%08x, expected 0x%08x", when, memory, index % 4u, index / 4u, output[index], expected); + Require(output[index] == expected, message); + } + }; + sample(0x3f800000u, "before the first texel of the block changed"); + Require(!watched || AgcDriver::GuestMemory::Watched(AddressOf(texels), SurfaceBytes), "comp-to-single keys sampled from write-watched memory: the first sampling left the surface unwatched"); + std::memset(texels + first, 0x00, 4); + sample(0u, "after the first texel of the block changed"); +} + +void RunSingleKeysOverPendingImage(AgcDriver::VulkanDevice& device, std::uint8_t* block, std::uint8_t* outputBlock) { +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + AgcDriver::Graphics::SetImportWatch(context, AgcDriver::Graphics::ImportWatch::Watch); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; +#endif + auto* texels = block; + auto* keys = block + KeysOffset; + auto* output = reinterpret_cast(outputBlock); + const auto surface = AddressOf(texels); + const AgcDriver::Graphics::ColorTargetLayout layout(Side, Side, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4); + std::memset(texels, 0x00, SurfaceBytes); + std::memset(texels + layout.Offset(1, 0) / 256 * 256, 0xff, 4); + std::memset(keys, 0xff, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + std::vector writeData(16, 0u); + std::copy(texture.begin(), texture.end(), writeData.begin() + 4); + const auto results = BufferDescriptor(output, Threads * 16u); + std::vector sampleData(24, 0u); + std::copy(results.begin(), results.end(), sampleData.begin()); + std::copy(texture.begin(), texture.end(), sampleData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), sampleData.begin() + 12); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Dispatch(device, WriteRowCode, writeData); + const auto image = StorageTexture::FindPending(surface, SurfaceBytes); + Require(image != nullptr && image->UploadedKeys() == DccKeys::Uncompressed, "comp-to-single keys over a pending storage image: the row write left no image pending under uncompressed keys"); + Require(AgcDriver::GuestMemory::Watched(surface, SurfaceBytes), "comp-to-single keys over a pending storage image: the image's host import left the surface unwatched"); + std::memset(keys, 0x10, KeyBytes); + std::fill(output, output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Dispatch(device, SampleSecondTexelCode, sampleData); + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "comp-to-single keys stored over a pending storage image: component %u of thread %u is 0x%08x, expected 0x3f800000", index % 4u, index / 4u, output[index]); + Require(output[index] == 0x3f800000u, message); + } +} + +void RunSampledKeysBehindKernel(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + std::memset(texels, 0x55, SurfaceBytes); + for (std::size_t i = 0; i < KeyBytes; i += 4) { + keys[i] = 0x00; + keys[i + 1] = 0x00; + keys[i + 2] = 0x80; + keys[i + 3] = 0x3f; + } + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + std::vector clearData(16, 0u); + const auto head = BufferDescriptor(keys, KeyBytes / 2); + std::copy(head.begin(), head.end(), clearData.begin()); + std::vector clearTailData(16, 0u); + const auto tail = BufferDescriptor(keys + KeyBytes / 2, KeyBytes / 2); + std::copy(tail.begin(), tail.end(), clearTailData.begin()); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Uncompressed, "sampled keys behind a kernel: key bytes that are no keys were not read as stored"); + Dispatch(device, ClearKeysCode, clearData, false); + Dispatch(device, ClearKeysCode, clearTailData, false); + const auto read = AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes); + char message[256]; + std::snprintf(message, sizeof(message), "a sampled texture whose keys a recorded kernel stores 0000 over read %s before the kernel ran", AgcDriver::Graphics::DccKeysName(read)); + Require(read == DccKeys::Clear0000, message); + device.WaitIdle(); + RequireKeys(keys, 0x00, "sampled keys behind a kernel: the keys after the kernel"); +} + +void RunUniformKeysBehindKernel(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + auto* texels = block; + auto* keys = block + KeysOffset; + std::memset(texels, 0x55, SurfaceBytes); + std::memset(keys, 0xff, KeyBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto texture = TextureDescriptor(texels, keys); + const auto resource = AgcDriver::Graphics::DecodeTextureResource(texture); + std::vector clearData(16, 0u); + const auto head = BufferDescriptor(keys, KeyBytes / 2); + std::copy(head.begin(), head.end(), clearData.begin()); + std::vector clearTailData(16, 0u); + const auto tail = BufferDescriptor(keys + KeyBytes / 2, KeyBytes / 2); + std::copy(tail.begin(), tail.end(), clearTailData.begin()); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes) == DccKeys::Uncompressed, "uniform sampled keys behind a kernel: the keys before the kernel were not read uncompressed"); + Dispatch(device, ClearKeysCode, clearData, false); + Dispatch(device, ClearKeysCode, clearTailData, false); + const auto read = AgcDriver::Graphics::TextureClearKeys(resource, SurfaceBytes); + char message[256]; + std::snprintf(message, sizeof(message), "a sampled texture whose uniform keys a recorded kernel stores 0000 over read %s before the kernel ran", AgcDriver::Graphics::DccKeysName(read)); + Require(read == DccKeys::Clear0000, message); + device.WaitIdle(); + RequireKeys(keys, 0x00, "uniform sampled keys behind a kernel: the keys after the kernel"); +} + void RunUntrackedReuse(AgcDriver::VulkanDevice& device, std::uint8_t* block) { const auto address = AddressOf(block); std::memset(block, 0x55, BlockBytes); @@ -501,6 +690,7 @@ int main() { std::optional untracked; std::optional shared; std::optional window; + std::optional pendingSingle; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; block.emplace(false); @@ -510,6 +700,20 @@ int main() { std::optional reused; reused.emplace(false); RunReusedKeys(*device, reused->Data()); + std::optional single; + single.emplace(false); + RunSingleKeys(*device, single->Data()); + std::optional sampled; + sampled.emplace(false); + std::optional sampledOutput; + sampledOutput.emplace(false); + RunSingleKeysSampled(*device, sampled->Data(), sampledOutput->Data(), "unwatched"); + std::optional behind; + behind.emplace(false); + RunSampledKeysBehindKernel(*device, behind->Data()); + std::optional uniform; + uniform.emplace(false); + RunUniformKeysBehindKernel(*device, uniform->Data()); #ifdef _WIN32 shared.emplace(true, true); RunSharedImport(*device, shared->Data()); @@ -518,6 +722,15 @@ int main() { if (AgcDriver::GuestMemory::WriteWatched()) { watched.emplace(true); Run(*device, watched->Data()); + std::optional sampledWatched; + sampledWatched.emplace(true); + RunSingleKeysSampled(*device, sampledWatched->Data(), sampledOutput->Data(), "write-watched"); +#ifdef _WIN32 + pendingSingle.emplace(true, true); +#else + pendingSingle.emplace(true); +#endif + RunSingleKeysOverPendingImage(*device, pendingSingle->Data(), sampledOutput->Data()); #ifndef _WIN32 window.emplace(true); RunImportWindow(*device, window->Data()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp index 159eb8223b..7d119f12f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp @@ -18,6 +18,17 @@ using DepthFastClearHarness::Width; alignas(4096) std::array Depth{}; alignas(256) std::array Htile{}; +alignas(16) std::array FillValue{}; +constexpr std::array MemoryValueFillKernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; + +void FillFromMemory(std::uint64_t htile, std::uint32_t groups, std::uint32_t value) { + FillValue[0] = value; + const auto valueAddress = reinterpret_cast(FillValue.data()); + const std::array userData{static_cast(htile), static_cast((htile >> 32u) & 0xffffu) | (4u << 16u), static_cast(DepthFastClearHarness::HtileBytes / 4u), 0x00014204u, static_cast(valueAddress), static_cast(valueAddress >> 32u) | (16u << 16u), 1u, 0x0004dfacu}; + const auto fill = AgcDriver::Graphics::MatchMemoryValueFill(MemoryValueFillKernel, userData, {64u, 1u, 1u}, {groups, 1u, 1u}); + DepthFastClearHarness::Require(fill.has_value(), "the HTILE fill from a value in memory was not recognized"); + AgcDriver::Graphics::NoteMemoryValueFill(*fill); +} } @@ -40,6 +51,14 @@ int main() { Require(Draw(*device, shaders, surface, {}) == 0u, "the depth written after a fast clear was lost"); AgcDriver::Graphics::NoteDepthMetadataFill(htile, HtileBytes, 0u); Require(Draw(*device, shaders, surface, {}) == Covered, "a second whole-HTILE fill of ZMask 0 did not clear the depth again"); + constexpr auto wholeHtileGroups = static_cast(HtileBytes / 4u / 64u); + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "the depth was not written before the memory value fills"); + FillFromMemory(htile, wholeHtileGroups, 0xffffffffu); + Require(Draw(*device, shaders, surface, {}) == 0u, "a memory value fill of ZMask 0xf cleared the depth"); + FillFromMemory(htile, wholeHtileGroups - 1u, 0u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a memory value fill shorter than the HTILE cleared the depth"); + FillFromMemory(htile, wholeHtileGroups, 0u); + Require(Draw(*device, shaders, surface, {}) == Covered, "a memory value fill of ZMask 0 over the whole HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); std::puts("depth fast clear tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp new file mode 100644 index 0000000000..a04e690b2c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -0,0 +1,326 @@ +#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/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.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 + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32Float = 22; +constexpr std::size_t LayerBytes = std::size_t{Side} * Side * 4u; +constexpr std::size_t BlockBytes = 2u * LayerBytes; +constexpr int Skipped = 77; + +PFN_vkGetDeviceProcAddr realDeviceProc = nullptr; +PFN_vkCmdCopyImageToBuffer realCopyImageToBuffer = nullptr; +PFN_vkCmdCopyBufferToImage realCopyBufferToImage = nullptr; +std::atomic imageToBufferCopies{0}; +std::atomic bufferToImageCopies{0}; + +VKAPI_ATTR void VKAPI_CALL CountImageToBuffer(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkBuffer buffer, std::uint32_t count, const VkBufferImageCopy* regions) { + imageToBufferCopies.fetch_add(1); + realCopyImageToBuffer(commands, image, layout, buffer, count, regions); +} + +VKAPI_ATTR void VKAPI_CALL CountBufferToImage(VkCommandBuffer commands, VkBuffer buffer, VkImage image, VkImageLayout layout, std::uint32_t count, const VkBufferImageCopy* regions) { + bufferToImageCopies.fetch_add(1); + realCopyBufferToImage(commands, buffer, image, layout, count, regions); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { + const auto function = realDeviceProc(device, name); + if (function == nullptr) return nullptr; + if (std::strcmp(name, "vkCmdCopyImageToBuffer") == 0) { + realCopyImageToBuffer = reinterpret_cast(function); + return reinterpret_cast(&CountImageToBuffer); + } + if (std::strcmp(name, "vkCmdCopyBufferToImage") == 0) { + realCopyBufferToImage = reinterpret_cast(function); + return reinterpret_cast(&CountBufferToImage); + } + return function; +} + +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()); + constexpr VkQueueFlags needed = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT; + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & needed) != needed) ++family; + Require(family < count, "no Vulkan graphics and 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"); + realDeviceProc = function("vkGetDeviceProcAddr"); + context.deviceProc = &CountingDeviceProc; + 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.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(); } + 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) ClearDepthSurfaces(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 WatchedBlock { +public: + WatchedBlock() { +#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, "depth plane guest layers: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~WatchedBlock() { + 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 + } + + WatchedBlock(const WatchedBlock&) = delete; + WatchedBlock& operator=(const WatchedBlock&) = 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::atomic watchedBegin{0}; +std::atomic watchedEnd{0}; +std::atomic watchedReads{0}; + +void CountReads(std::uint64_t address, std::size_t bytes) { + if (address < watchedEnd.load() && address + bytes > watchedBegin.load()) watchedReads.fetch_add(1); +} + +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 resource{}; + resource.baseAddress = address; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 1; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2DArray; + resource.format = Format32Float; + resource.dstSelX = 4; + 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}; + watchedBegin = address + LayerBytes; + watchedEnd = address + BlockBytes; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + const auto sample = [&](const char* when) { + watchedReads = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, std::string("the 2D array over the depth surface was not sampled through its depth planes ") + when); + return watchedReads.load(); + }; + try { + Require(sample("at its first use") != 0, "the first sampling of the depth planes did not read the layer no depth surface holds"); + Require(sample("again") == 0, "sampling the depth planes again read the guest layer no depth surface holds although nothing wrote it"); + FillLayer(block.Data() + LayerBytes, 0.75f); + Require(sample("after a CPU write") != 0, "sampling the depth planes after a CPU write to the guest layer did not read it again"); + Require(sample("after the re-read") == 0, "sampling the depth planes after the re-read of the written layer read it once more"); + AgcDriver::GuestMemory::MarkWritten(address + LayerBytes, 4096); + Require(sample("after a driver store") != 0, "sampling the depth planes after a driver store over the guest layer did not read it again"); + const DepthTarget slice{address, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}; + const DepthTarget other{address + 2 * BlockBytes, 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}; + const auto copies = [&](const char* when, std::uint32_t fromSlices, std::uint32_t intoCopy) { + imageToBufferCopies = 0; + bufferToImageCopies = 0; + sample(when); + if (imageToBufferCopies.load() != fromSlices || bufferToImageCopies.load() != intoCopy) { + throw std::runtime_error(std::string("sampling the depth planes ") + when + " recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected " + std::to_string(fromSlices) + " and " + std::to_string(intoCopy)); + } + }; + DepthSurfaceView(context, other); + copies("after a draw to another depth surface", 1, 1); + copies("with nothing written since", 0, 0); + DepthSurfaceView(context, slice); + copies("while a draw to its depth surface is being recorded", 1, 1); + copies("again while that draw is the last depth draw", 1, 1); + DepthSurfaceView(context, other); + copies("after that draw", 1, 1); + copies("with nothing written since that draw", 0, 0); + FillLayer(block.Data() + LayerBytes, 0.5f); + copies("after a CPU write to the guest layer only", 0, 1); + copies("with nothing written since the CPU write", 0, 0); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); +} + +} + +int main() { + 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; + } + { + WatchedBlock block; + Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); + Run(device->GetContext(), block); + } + device.reset(); + std::puts("depth plane guest layer 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/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp new file mode 100644 index 0000000000..2cdf03f889 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -0,0 +1,353 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "DepthFastClearHarness.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 DepthFastClearHarness::BufferDescriptor; +using DepthFastClearHarness::PixelCode; +using DepthFastClearHarness::VertexCode; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format16Uint = 11; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t TypeCube = 11; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t Swizzle64KbS = 9; +constexpr std::uint32_t Swizzle64KbZX = 24; +constexpr std::uint64_t SliceBytes = 65536; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::size_t OutputOffset = 0; +constexpr std::size_t ArrayOffset = 0x10000; +constexpr std::size_t CubeOffset = 0x40000; +constexpr std::size_t IntegerOffset = 0xa0000; +constexpr std::size_t DepthBitsOffset = 0xb0000; +constexpr std::size_t OneSliceOffset = 0xd0000; +constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr float DrawnDepth = 0.25f; +constexpr float DrawnD16Depth = 0.2f; +constexpr std::uint32_t DrawnD16Bits = 13107; + +// image_load of row 0, one texel per thread (x = thread id), slice / face from user data, stored as a dword per +// thread into the buffer at s[0:3]. The T# is s[4:11], the slice s12. +constexpr std::array LoadDims{1u, 3u, 5u}; + +std::array LoadCode(std::uint32_t dim) { + return { + 0x7e020280, // v_mov_b32 v1, 0 (y) + 0x7e04020c, // v_mov_b32 v2, s12 (slice or face) + 0xf0001100u | (dim << 3u), // image_load v3, v[0:2], s[4:11] dmask:0x1 unorm, dim 2D / CUBE / 2D_ARRAY + 0x00010300, + 0xbf8c3f70, // s_waitcnt vmcnt(0) + 0x34080082, // v_lshlrev_b32 v4, 2, v0 + 0xe0701000, // buffer_store_dword v3, v4, s[0:3], 0 offen + 0x80000304, + 0xbf810000, // s_endpgm + 0xbf810000, + 0xbf810000, + }; +} + +alignas(256) std::array Load2D = LoadCode(LoadDims[0]); +alignas(256) std::array LoadCube = LoadCode(LoadDims[1]); +alignas(256) std::array LoadArray = LoadCode(LoadDims[2]); + +alignas(256) std::array Pixels{}; + +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, "depth plane sampling: cannot allocate the guest block"); + 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* At(std::size_t offset) { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Shape { + std::uint32_t format; + std::uint32_t width; + std::uint32_t height; + std::uint32_t type; + std::uint32_t lastArray; + std::uint32_t swizzle; + std::uint32_t baseArray = 0; +}; + +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 | (shape.swizzle << 20u) | (shape.type << 28u), + shape.lastArray | (shape.baseArray << 16u), + 0u, 0u, 0u, + }; +} + +std::uint32_t Bits(float value) { + std::uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; +} + +// Draws a full-target triangle at depth `z` into the depth surface at `address`, which makes the driver create the +// surface (the base slice a later plane copy reads from the surface instead of from guest memory). +void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2D extent, VkFormat format, float z) { + static std::deque>> vertexData; + vertexData.push_back({{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}); + const auto& vertices = vertexData.back(); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {32, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixelInfo.posX = true; + pixelInfo.posY = true; + pixelInfo.targetOutputMode[0] = 9; + pixelInfo.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(4, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragmentRequest{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {32, 0, pixelUserData, std::nullopt, pixelInfo, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragmentRequest.useCache = false; + const auto pixel = ShaderRecompiler::Recompile(fragmentRequest); + const std::array shaders{{ + {ShaderStage::Vertex, &vertex, 0}, + {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, static_cast(extent.width) * extent.height * 4u, 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{address, 0, extent, format, 1.0f, 0}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.renderExtent = extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(extent.height), static_cast(extent.width), -static_cast(extent.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, extent}; + 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(vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +// Loads row 0 of `slice` of the texture `shape` describes at `texels` into `output` (one dword per thread). With +// `shaderImage2D` the shader declares a 2D image whatever the T#'s type is. +void Load(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t slice, std::span output, bool shaderImage2D = false) { + const auto& code = shaderImage2D ? Load2D : shape.type == TypeCube ? LoadCube : shape.type == Type2DArray ? LoadArray : Load2D; + std::vector userData(16, 0u); + const auto out = std::array{static_cast(reinterpret_cast(output.data())), static_cast((reinterpret_cast(output.data()) >> 32u) & 0xffffu), static_cast(output.size_bytes()), 0x31016facu}; + const auto texture = TextureDescriptor(texels, shape); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = slice; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(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; + std::fill(output.begin(), output.end(), 0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Expect(std::span output, std::uint32_t expected, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + char text[160]; + std::snprintf(text, sizeof(text), "%s: texel %u read 0x%08x, expected 0x%08x", what.c_str(), tid, output[tid], expected); + Require(output[tid] == expected, text); + } +} + +void FillSlice(std::uint8_t* slice, std::uint64_t bytes, std::uint32_t value, std::uint32_t elementBytes) { + for (std::uint64_t offset = 0; offset < bytes; offset += elementBytes) std::memcpy(slice + offset, &value, elementBytes); +} + +// A 3-slice D32 array: slice 0 from the live depth surface, slices 1 and 2 from guest memory (detiled). +void ArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(ArrayOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + FillSlice(base + SliceBytes, SliceBytes, Bits(0.5f), 4u); + FillSlice(base + 2u * SliceBytes, SliceBytes, Bits(0.75f), 4u); + const Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + const std::array expected{DrawnDepth, 0.5f, 0.75f}; + for (std::uint32_t slice = 0; slice < 3u; ++slice) { + Load(device, base, shape, slice, output); + Expect(output, Bits(expected[slice]), "D32 2D array slice " + std::to_string(slice)); + } +} + +// A cube over six square D32 slices: face 0 from the live surface, faces 1-5 from guest memory. +void CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(CubeOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, Bits(static_cast(face) / 8.0f), 4u); + const Shape shape{Format32Float, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, Bits(face == 0 ? DrawnDepth : static_cast(face) / 8.0f), "D32 cube face " + std::to_string(face)); + } +} + +// A D16 plane read through an R16_UINT view (a non-depth swizzle, so not flagged as depth bits): the raw depth value. +void IntegerCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(IntegerOffset); + DrawDepth(device, reinterpret_cast(base), {256, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + Load(device, base, {Format16Uint, 256, 128, Type2D, 0, Swizzle64KbS}, 0, output); + Expect(output, DrawnD16Bits, "D16 plane as R16_UINT"); +} + +// A D16 2D array whose R16_UINT T# has a depth swizzle (flagged as depth bits): both slices' raw values. +void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(DepthBitsOffset); + DrawDepth(device, reinterpret_cast(base), {256, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + FillSlice(base + SliceBytes, SliceBytes, 0x1234u, 2u); + const Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + Load(device, base, shape, 0, output); + Expect(output, DrawnD16Bits, "D16 depth-bits array slice 0"); + Load(device, base, shape, 1, output); + Expect(output, 0x1234u, "D16 depth-bits array slice 1"); +} + +// A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth +// image itself, and a 3-slice array, which samples the plane copy. +void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* one = block.At(OneSliceOffset); + DrawDepth(device, reinterpret_cast(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + Load(device, one, {Format32Float, 128, 128, Type2DArray, 0, Swizzle64KbZX}, 0, output, true); + Expect(output, Bits(DrawnDepth), "one-slice D32 2D array through a 2D shader image"); + auto* base = block.At(FirstSliceOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); + FillSlice(base + SliceBytes, SliceBytes, Bits(0.5f), 4u); + FillSlice(base + 2u * SliceBytes, SliceBytes, Bits(0.75f), 4u); + Load(device, base, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 0, output, true); + Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); +} + +// A plane array from a later base slice is still refused (its slice offsets are not modeled here). +void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + bool refused = false; + try { + Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, block.At(ArrayOffset), shape, 0, output); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("sampling the depth planes of depth surface") != std::string::npos; + if (!refused) throw; + } + Require(refused, "a depth plane array from base slice 1 was not refused"); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const std::span output(reinterpret_cast(block.At(OutputOffset)), Threads); + ArrayCase(*device, block, output); + CubeCase(*device, block, output); + IntegerCase(*device, block, output); + DepthBitsArrayCase(*device, block, output); + FirstSliceCase(*device, block, output); + RefusalCase(*device, block, output); + std::puts("depth plane sampling 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/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp new file mode 100644 index 0000000000..2c0031e06c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -0,0 +1,281 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.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/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::uint32_t Side = 64; +constexpr std::size_t BlockBytes = 65536; +alignas(4096) std::array Storage{}; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format8Uint = 5; + +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"); + const auto hasExtension = [&](const char* name) { + for (const auto& extension : available) { + if (std::strcmp(extension.extensionName, name) == 0) return true; + } + return false; + }; + 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; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { + extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); + VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &properties); + context.hostImportAlignment = hostProperties.minImportedHostPointerAlignment; + } + 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"); + if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); + 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(); } + 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{}; +}; + + +template +void ExpectFailure(TAction action, const char* expected, const char* message) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, error.what()); + return; + } + throw std::runtime_error(message); +} + +GuestTextureResource PlaneResource(std::uint64_t address, std::uint32_t format, std::uint32_t side = Side) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = side; + resource.height = side; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::size_t CountFloats(const std::uint8_t* bytes, float value) { + std::size_t count = 0; + for (std::size_t i = 0; i < Side * Side; ++i) { + float texel = 0; + std::memcpy(&texel, bytes + i * sizeof(float), sizeof(texel)); + if (texel == value) ++count; + } + return count; +} + +void Run(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast(block); + const auto stencilAddress = address + BlockBytes / 2; + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget target{address, stencilAddress, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + Require(DepthSurfaceView(context, target) != VK_NULL_HANDLE, "depth surface was not created"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float, Side / 2), 0) == nullptr, "a storage view of another extent over the depth memory was taken for the depth surface"); + Require(DepthSurfaceTexture(context, {}, PlaneResource(address, Format32Float, Side / 2), {}) == nullptr, "a sampled view of another extent over the depth memory was taken for the depth surface"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format8Uint), 0) == nullptr, "an 8-bit storage view of the depth plane was taken for the depth surface"); + ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 1)); }, "not implemented", "a storage view of another level of the depth plane was accepted"); + const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(depth != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*depth); + Require(CountFloats(block, 0.25f) == Side * Side, "the storage image did not receive the cleared depth"); + const std::array pattern{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the depth plane's storage image was not stored back"); + submit(recorder); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0) == depth, "the depth plane's storage image was not reused"); + submit(recorder); + readBack(*depth); + Require(CountFloats(block, 0.75f) == Side * Side, "the stored depth did not come back from the depth surface"); + const auto stencil = DepthSurfaceStorage(context, {}, PlaneResource(stencilAddress, Format8Uint), 0); + Require(stencil != nullptr && stencil != depth, "the stencil plane has no storage image of its own"); + submit(recorder); + readBack(*stencil); + const auto stencilBytes = DescribeSurface(PlaneResource(stencilAddress, Format8Uint)).guestBytes; + Require(stencilBytes <= BlockBytes / 2 && std::count(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, 7) == static_cast(Side * Side), "the storage image did not receive the cleared stencil"); + const std::array half{std::bit_cast(0.5f), std::bit_cast(0.5f), std::bit_cast(0.5f), std::bit_cast(0.5f)}; + Require(depth->FillClear(half, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the depth plane's storage image was not stored back"); + submit(recorder); + auto layeredDepth = PlaneResource(address, Format32Float); + layeredDepth.dimension = TextureDimension::k2DArray; + const auto arrayDepth = DepthSurfaceStorage(context, {}, layeredDepth, 0); + Require(arrayDepth != nullptr && arrayDepth != depth, "a one-layer 2D array view of the depth plane was not taken for the depth surface as a storage image of its own"); + ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through the 2D view that the one-layer 2D array view replaced was accepted"); + submit(recorder); + readBack(*arrayDepth); + Require(CountFloats(block, 0.5f) == Side * Side, "the one-layer 2D array view did not receive the depth plane"); + std::fill(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, std::uint8_t{0}); + auto layeredStencil = PlaneResource(stencilAddress, Format8Uint); + layeredStencil.dimension = TextureDimension::k2DArray; + const auto arrayStencil = DepthSurfaceStorage(context, {}, layeredStencil, 0); + Require(arrayStencil != nullptr, "a one-layer 2D array view of the stencil plane was not taken for the depth surface"); + submit(recorder); + readBack(*arrayStencil); + Require(std::count(block + BlockBytes / 2, block + BlockBytes / 2 + stencilBytes, 7) == static_cast(Side * Side), "the one-layer 2D array view did not receive the stencil plane"); + 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"); + 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"); +} + +} + +int main() { + try { + 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; + } + auto context = device->GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + auto* block = Storage.data() + (BlockBytes - reinterpret_cast(Storage.data()) % BlockBytes) % BlockBytes; + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Run(context, block); + ClearDepthSurfaces(context.device); + } + GuestAllocations::Mutation().Remove(block); + std::cout << "depth surface storage access 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/execution/DepthStorageDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp new file mode 100644 index 0000000000..9b8dba980d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorageDispatch.cpp @@ -0,0 +1,346 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.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 + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Pixels; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Format8Uint = 5; +constexpr std::uint32_t Format32Float = 22; +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::size_t StencilOffset = 1u << 16u; +constexpr std::size_t OutputOffset = 2u << 16u; +constexpr std::size_t MultisampledOffset = 4u << 16u; +constexpr std::size_t MultisampledColorOffset = 5u << 16u; +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; + +alignas(256) constexpr std::array ScaleDepthCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x1014140cu, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AddStencilCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e140280u, 0xf0001108u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x4a141484u, 0xf0201108u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ScaleDepthLayerCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e400280u, 0x7e140280u, 0xf0001128u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x1014140cu, 0xf0201128u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AddStencilLayerCode{ + 0x7e3c0300u, 0x7e3e0280u, 0x7e400280u, 0x7e140280u, 0xf0001128u, 0x00010a1eu, 0xbf8c3f70u, 0x34060082u, + 0xe0701000u, 0x80000a03u, 0x4a141484u, 0xf0201128u, 0x00010a1eu, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreLayerCode{ + 0x7e280d00u, 0x1028280eu, 0x7e3c0300u, 0x7e3e0280u, 0x7e40020cu, 0x7e42020du, 0xf0241128u, 0x0001141eu, 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array StoreTwoViewsCode{ + 0x7e280d00u, 0x102828ffu, 0x3c800000u, 0x7e3c0300u, 0x7e3e0280u, 0xf0201108u, 0x0001141eu, 0xf0201108u, 0x0003141eu, 0xbf810000u, +}; + +alignas(256) constexpr std::array LoadLayerCode{ + 0x7e020280u, 0x7e04020cu, 0xf0001128u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "depth storage dispatch: 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; + + 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, std::uint32_t format, std::uint32_t type = Type2D, std::uint32_t tile = TileLinear, std::uint32_t lastArray = 0) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (tile << 20u) | (type << 28u), + lastArray, 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{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +ShaderRecompiler::RecompileResult PlaneProgram(AgcDriver::VulkanDevice& device, std::span code, std::span output, const std::uint8_t* plane, std::uint32_t format, float factor) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(plane, format); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = std::bit_cast(factor); + return Compile(device, code, userData); +} + +void RunPlane(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::span code, std::span output, std::uint32_t expected, const std::string& what) { + std::fill(output.begin(), output.end(), 0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(output[lane] == expected, what + ": the dispatch read " + std::to_string(output[lane]) + " at texel " + std::to_string(lane) + ", expected " + std::to_string(expected)); + } +} + +void RequireRow(std::uint32_t covered, const std::string& what) { + Require(covered == Threads, what + ": " + std::to_string(covered) + " pixels passed, expected the " + std::to_string(Threads) + " the dispatch stored"); + for (std::uint32_t x = 0; x < Threads; ++x) { + Require(std::to_integer(Pixels[x * 4u]) == 255u, what + ": pixel " + std::to_string(x) + " of the first row did not pass"); + } +} + +template +void ExpectRefusal(TAction action, const char* expected, const std::string& what) { + try { + action(); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, what + " threw another error: " + error.what()); + return; + } + throw std::runtime_error(what + " was accepted"); +} + +VkStencilOpState StencilEqual(std::uint32_t reference) { + return {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, reference}; +} + +void StoreBackCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block; + auto* stencil = block + StencilOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), reinterpret_cast(stencil), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const auto bits = [](float value) { return std::bit_cast(value); }; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, "the depth and stencil producer did not cover the surface"); + + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + const auto lower = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 0.5f); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.5f), "first depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the first dispatch's depth"); + RunPlane(device, lower, ScaleDepthCode, output, bits(0.75f), "second depth dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}), "depth test against the second dispatch's depth"); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == 0, "the first dispatch's depth outlived the second dispatch"); + RunPlane(device, raise, ScaleDepthCode, output, bits(0.375f), "third depth dispatch, its program's second"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test against the third dispatch's depth"); + + const auto add = PlaneProgram(device, AddStencilCode, output, stencil, Format8Uint, 0.0f); + RunPlane(device, add, AddStencilCode, output, 5u, "first stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(9u)}), "stencil test against the first dispatch's stencil"); + RunPlane(device, add, AddStencilCode, output, 9u, "second stencil dispatch"); + RequireRow(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(13u)}), "stencil test against the second dispatch's stencil"); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = StencilEqual(5u)}) == Covered - Threads, "a stencil dispatch changed stencil values it did not store"); + RequireRow(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}), "depth test after the stencil dispatches"); + + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(depth), Width * Height * sizeof(float), nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + float texel = 0.0f; + std::memcpy(&texel, depth + x * sizeof(float), sizeof(texel)); + Require(texel == (x < Threads ? 0.5625f : 0.5f), "guest depth texel " + std::to_string(x) + " is " + std::to_string(texel) + " after the flush"); + } + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(stencil), Width * Height, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t x = 0; x < Width; ++x) { + Require(stencil[x] == (x < Threads ? 13u : 5u), "guest stencil texel " + std::to_string(x) + " is " + std::to_string(stencil[x]) + " after the flush"); + } +} + +void ArrayViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block + ArrayDepthOffset; + auto* stencil = block + ArrayStencilOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), reinterpret_cast(stencil), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, "the depth and stencil producer of the array views did not cover the surface"); + const auto layer = [&](std::span code, const std::uint8_t* plane, std::uint32_t format, float factor) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto texture = TextureDescriptor(plane, format, Type2DArray); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = std::bit_cast(factor); + return Compile(device, code, userData); + }; + RunPlane(device, layer(ScaleDepthLayerCode, depth, Format32Float, 1.5f), ScaleDepthLayerCode, output, std::bit_cast(0.5f), "depth dispatch through a one-layer 2D array view"); + 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"); + + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + 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"); +} + +void MultisampledCase(AgcDriver::VulkanDevice& device, const Shaders& compiled, std::uint8_t* block) { + auto* depth = block + MultisampledOffset; + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(block + MultisampledColorOffset), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Width * Height * 16u, 0xe4u}; + state.color.samples = 4; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(depth), 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false, 4}; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_ALWAYS; + state.samples = 4; + 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(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(depth, Format32Float); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const auto program = Compile(device, StoreCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); }, "storage image access to a multisampled depth surface", "a storage view of a multisampled depth plane"); +} + +void SampledCopyCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, std::uint8_t* block) { + auto* depth = block + SampledOffset; + const std::span output(reinterpret_cast(block + OutputOffset), Threads); + const Surface surface{reinterpret_cast(depth), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const Surface later{reinterpret_cast(block + LaterDepthOffset), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the sampled depth producer did not cover the surface"); + Require(Draw(device, shaders, later, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the later depth producer did not cover its surface"); + + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(output.data(), static_cast(output.size_bytes())); + const auto layers = TextureDescriptor(depth, Format32Float, Type2DArray, TileLinear, 1u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(layers.begin(), layers.end(), userData.begin() + 4); + const auto load = Compile(device, LoadLayerCode, userData); + const auto raise = PlaneProgram(device, ScaleDepthCode, output, depth, Format32Float, 1.5f); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the depth plane copy"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.5f), "first layer of the unchanged depth plane copy"); + RunPlane(device, raise, ScaleDepthCode, output, std::bit_cast(0.5f), "depth dispatch between two plane copies"); + RunPlane(device, load, LoadLayerCode, output, std::bit_cast(0.75f), "first layer of the depth plane copy after the dispatch stored to the plane"); +} + +void TwoViewsCase(AgcDriver::VulkanDevice& device, std::uint8_t* block) { + std::vector userData(20, 0u); + const auto first = TextureDescriptor(block, Format32Float, Type2D, TileLinear); + const auto second = TextureDescriptor(block, Format32Float, Type2D, TileStandard256B); + std::copy(first.begin(), first.end(), userData.begin() + 4); + std::copy(second.begin(), second.end(), userData.begin() + 12); + const auto program = Compile(device, StoreTwoViewsCode, userData); + ExpectRefusal([&] { device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreTwoViewsCode.data())); }, "through a view that another view of the plane", "a dispatch storing to two views of one depth plane"); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + StoreBackCase(*device, shaders, block.Data()); + ArrayViewCase(*device, shaders, block.Data()); + MultisampledCase(*device, shaders, block.Data()); + TwoViewsCase(*device, block.Data()); + SampledCopyCase(*device, shaders, block.Data()); + std::puts("depth storage dispatch 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/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/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index cc34520709..276ea252d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -25,6 +25,9 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Side = 2; constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Format8UNorm = 1; +constexpr std::uint32_t Format8_8UNorm = 14; +constexpr std::uint32_t Format1616UNorm = 23; constexpr std::uint32_t Format9995Float = 132; constexpr std::uint32_t Format8Srgb = 128; constexpr std::uint32_t Format8_8Srgb = 129; @@ -34,6 +37,7 @@ constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t TypeCube = 11; constexpr std::uint32_t LessEqual = 3; +constexpr std::uint32_t GreaterEqual = 6; constexpr std::uint32_t ClampWrap = 0; constexpr std::uint32_t ClampMirror = 1; constexpr std::uint32_t ClampEdge = 2; @@ -121,14 +125,14 @@ std::array SamplerDescriptor(const Sampler& sampler) { void FillTexels() { Texels.fill(0); - const auto descriptor = TextureDescriptor(Format8888UNorm); + const auto descriptor = TextureDescriptor(Format1616UNorm); const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); const auto& mip = surface.mips.at(0); for (std::uint32_t y = 0; y < Side; ++y) { for (std::uint32_t x = 0; x < Side; ++x) { auto* texel = Texels.data() + mip.tiledOffset + y * mip.pitchBytes + x * 4u; texel[0] = Red[y * Side + x]; - texel[1] = 0x11; + texel[1] = Red[y * Side + x]; texel[2] = 0x22; texel[3] = 0xff; } @@ -137,7 +141,7 @@ void FillTexels() { void FillCubeTexels() { Texels.fill(0); - const auto descriptor = TextureDescriptor(Format8888UNorm, true); + const auto descriptor = TextureDescriptor(Format1616UNorm, true); const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); Require(surface.layers == 6u && surface.guestBytes <= Texels.size(), "cube descriptor did not expose six faces within texture storage"); const auto& mip = surface.mips.at(0); @@ -146,6 +150,7 @@ void FillCubeTexels() { for (std::uint32_t x = 0; x < Side; ++x) { auto* texel = Texels.data() + surface.GuestLayerOffset(face) + mip.tiledOffset + y * mip.pitchBytes + x * 4u; texel[0] = static_cast(face == 0u ? 255u : 0u); + texel[1] = texel[0]; texel[3] = 0xffu; } } @@ -240,7 +245,7 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false, std::uint64_t expectedVariant = 0) { Output.fill(-1.0f); const std::span code = offsets ? std::span(OffsetCode) : array ? std::span(ArrayCode) : std::span(Code); - const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); + const auto result = Compile(device, Format1616UNorm, sampler, code, useCache); if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); @@ -264,7 +269,7 @@ void RunChannels(AgcDriver::VulkanDevice& device, std::span const Sampler sampler{ClampEdge, FilterBilinear}; Swizzle = Select(channels[0], channels[1], channels[2], channels[3]); Output.fill(-2.0f); - const auto result = Compile(device, Format8888UNorm, sampler, code); + const auto result = Compile(device, Format1616UNorm, sampler, code); Swizzle = 0xfacu; device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -291,7 +296,7 @@ void RunCube(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char CubeInput[tid * 4u + 3u] = 0.0f; } Output.fill(-1.0f); - const auto result = Compile(device, Format8888UNorm, sampler, CubeCode, false, true, true); + const auto result = Compile(device, Format1616UNorm, sampler, CubeCode, false, true, true); for (const auto& binding : result.bindings) { Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "cube emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "cube emulated comparison retained an unused sampler association"); @@ -360,7 +365,7 @@ int main() { FillTexels(); FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); - Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); + Reject(*device, Format1616UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); for (std::uint32_t function = 0; function < 8u; ++function) { const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function); RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); @@ -369,8 +374,13 @@ int main() { RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP"); Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats"); - Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); - Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear"); + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "bilinear 8_8_8_8 unorm cube function " + std::to_string(function); + RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + } + RunSrgbCubeZero(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear, BorderBlack, 0u, 0u, false, GreaterEqual}, "anisotropic bilinear 8_8_8_8 unorm cube without DISABLE_CUBE_WRAP"); + Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); + Reject(*device, Format1616UNorm, {ClampEdge, FilterAnisoPoint}, "point or bilinear"); FillTexels(); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); @@ -387,6 +397,15 @@ int main() { } if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant); } + for (const auto format : {Format8UNorm, Format8_8UNorm, Format8888UNorm}) { + for (std::uint32_t function = 0; function < 8u; ++function) { + for (const auto filter : {FilterPoint, FilterBilinear, FilterAnisoBilinear}) { + if (format != Format8888UNorm && function != LessEqual) continue; + const auto name = "8-bit unorm format " + std::to_string(format) + " function " + std::to_string(function) + " filter " + std::to_string(filter); + RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), false, 0u); + } + } + } Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp new file mode 100644 index 0000000000..b826f18fb6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16ToF32Unpack.cpp @@ -0,0 +1,209 @@ +#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 +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Halves = 1u << 16u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ConvertCode{ + 0x34020082, 0x8f098708, 0x4a020209, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e0a1704, 0xe0701000, + 0x80010501, 0xbf810000, +}; + +alignas(256) constexpr std::array ByteCode{ + 0x34020082, 0x8f098708, 0x4a020209, 0xe0301000, 0x80000401, 0x7e0e02ff, 0x00008000, 0xbf8c3f70, + 0x640c0ef9, 0x06010604, 0xe0701000, 0x80010601, 0xbf810000, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020082, 0x34040083, 0x8f098708, 0x8f0a8808, 0x4a020209, 0x4a04040a, 0xe0301000, 0x80000401, + 0x7e0c02ff, 0x00003c00, 0xbf8c3f70, 0x7dd00904, 0xd5010005, 0x01a90280, 0x7d920d04, 0xd5010007, + 0x01a90280, 0xe0701000, 0x80010502, 0xe0701004, 0x80010702, 0xbf810000, +}; + +std::uint32_t HalfToFloatBits(std::uint32_t half) { + const std::uint32_t sign = (half & 0x8000u) << 16u; + const std::uint32_t exponent = (half >> 10u) & 0x1fu; + const std::uint32_t mantissa = half & 0x3ffu; + if (exponent == 0x1fu) return sign | 0x7f800000u | (mantissa << 13u); + if (exponent != 0u) return sign | ((exponent + 112u) << 23u) | (mantissa << 13u); + return sign | std::bit_cast(static_cast(mantissa) * 0x1p-24f); +} + +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::vector UserData() { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + return userData; +} + +ShaderRecompiler::RecompileRequest Request(std::span code, const ShaderRecompiler::SpirvTarget& target, std::span userData, std::span memory) { + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}; + return request; +} + +struct F16Decode { + std::uint32_t unpacks = 0; + std::uint32_t nanRebuilds = 0; + bool denormPreserve16 = false; +}; + +F16Decode ScanF16Decode(std::span words) { + F16Decode decode; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + Require(count != 0 && cursor + count <= words.size(), "f16 to f32 unpack: malformed SPIR-V"); + if (opcode == spv::OpExecutionMode && count == 4 && words[cursor + 2] == spv::ExecutionModeDenormPreserve && words[cursor + 3] == 16u) decode.denormPreserve16 = true; + if (opcode == spv::OpExtInst && count >= 5 && words[cursor + 4] == GLSLstd450UnpackHalf2x16) ++decode.unpacks; + if (opcode == spv::OpShiftRightArithmetic) ++decode.nanRebuilds; + cursor += count; + } + return decode; +} + +void VerifySpirv() { + constexpr std::array capabilities{ + spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + spv::CapabilityDenormPreserve, spv::CapabilitySignedZeroInfNanPreserve, + }; + constexpr std::array extensions{ + "SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", + "SPV_KHR_storage_buffer_storage_class", + }; + const auto userData = UserData(); + ShaderRecompiler::SpirvTarget target{}; + target.vulkanVersion = 0x00403000u; + target.spirvVersion = 0x00010600u; + target.subgroupSize = 32; + target.bdaAbiVersion = 1; + target.supportedCapabilities = capabilities; + target.supportedExtensions = extensions; + target.maxWorkgroupSize = {1024, 1024, 64}; + target.maxWorkgroupInvocations = 1024; + target.maxWorkgroupSharedMemoryBytes = 49152; + struct Program { + std::span code; + const char* name; + std::optional nanBits; + }; + const std::array programs{{ + {ConvertCode, "v_cvt_f32_f16", true}, + {ByteCode, "v_add_f16 src0_sel:BYTE_1", std::nullopt}, + {CompareCode, "v_cmp_u_f16 and v_cmp_lt_f16", false}, + }}; + for (const auto& program : programs) { + const std::array memory{{{reinterpret_cast(program.code.data()), std::as_bytes(program.code)}}}; + for (const bool preserve : {false, true}) { + target.float16DenormPreserve = preserve; + const std::string flag = std::string(" with float16DenormPreserve=") + (preserve ? "true" : "false"); + const auto decode = ScanF16Decode(ShaderRecompiler::Recompile(Request(program.code, target, userData, memory)).spirv.Words()); + const std::string name = std::string("f16 to f32 unpack: ") + program.name; + Require(decode.denormPreserve16 == preserve, name + (preserve ? " lacks" : " has") + " DenormPreserve 16" + flag); + Require(preserve ? decode.unpacks != 0u : decode.unpacks == 0u, name + " has " + std::to_string(decode.unpacks) + " UnpackHalf2x16" + flag); + if (program.nanBits.has_value()) { + Require(decode.nanRebuilds == (preserve && *program.nanBits ? decode.unpacks : 0u), name + " rebuilds " + std::to_string(decode.nanRebuilds) + " of " + std::to_string(decode.unpacks) + " NaN results" + flag); + } + } + } +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const ShaderRecompiler::SpirvTarget& target) { + const auto userData = UserData(); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const auto result = ShaderRecompiler::Recompile(Request(code, target, userData, memory)); + Output.fill(0xdeadbeefu); + device.Dispatch(result, Halves / Threads, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, const char* name) { + Dispatch(device, ConvertCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + Require(Output[lane] == HalfToFloatBits(lane), std::string("f16 to f32 unpack (") + name + "): v_cvt_f32_f16 of " + Hex(Input[lane]) + " is " + Hex(Output[lane]) + ", expected " + Hex(HalfToFloatBits(lane))); + } + Dispatch(device, ByteCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + const std::uint32_t selected = (Input[lane] >> 8u) & 0xffu; + Require((Output[lane] & 0xffffu) == selected, std::string("f16 to f32 unpack (") + name + "): v_add_f16 src0_sel:BYTE_1 of " + Hex(Input[lane]) + " and -0 is " + Hex(Output[lane]) + ", expected " + Hex(selected)); + } + Dispatch(device, CompareCode, target); + for (std::uint32_t lane = 0; lane < Halves; ++lane) { + const float value = std::bit_cast(HalfToFloatBits(lane)); + const std::uint32_t unordered = value != value ? 1u : 0u; + const std::uint32_t less = value < 1.0f ? 1u : 0u; + Require(Output[lane * 2u] == unordered, std::string("f16 to f32 unpack (") + name + "): v_cmp_u_f16 of " + Hex(lane) + " with itself is " + Hex(Output[lane * 2u]) + ", expected " + Hex(unordered)); + Require(Output[lane * 2u + 1u] == less, std::string("f16 to f32 unpack (") + name + "): v_cmp_lt_f16 of " + Hex(lane) + " and 1.0 is " + Hex(Output[lane * 2u + 1u]) + ", expected " + Hex(less)); + } +} + +} + +int main(int argc, char** argv) { + try { + if (argc == 2 && std::string_view(argv[1]) == "--verify-spirv") { + VerifySpirv(); + std::puts("f16 to f32 conversions use UnpackHalf2x16 only under DenormPreserve 16"); + return 0; + } + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t lane = 0; lane < Halves; ++lane) Input[lane] = lane | ((lane ^ 0xa5a5u) << 16u); + auto target = device->Target(); + const bool preserve = target.float16DenormPreserve; + target.float16DenormPreserve = false; + Run(*device, target, "integer decode"); + if (preserve) { + target.float16DenormPreserve = true; + Run(*device, target, "UnpackHalf2x16"); + } + std::printf("f16 to f32 unpack tests passed (%u halves, %s)\n", Halves, preserve ? "both decodes" : "integer decode only"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp index ad9657aedb..26f1be254b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Modifiers.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include namespace { @@ -35,6 +37,11 @@ alignas(256) constexpr std::array Code{ 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; +alignas(256) constexpr std::array Min3Div2{0xd7510001, 0x1c020100, 0xbf810000}; +alignas(256) constexpr std::array Max3Mul2{0xd7540001, 0x0c020100, 0xbf810000}; +alignas(256) constexpr std::array Med3Mul4{0xd7570001, 0x14020100, 0xbf810000}; +alignas(256) constexpr std::array DivFixupMul2{0xd75f0001, 0x0c020100, 0xbf810000}; + constexpr std::uint32_t Rows[32][4] = { {0x00000000u, 0x0000ec99u, 0x000073abu, 0x0000965eu}, {0x0000c000u, 0x000079cbu, 0x0000cb00u, 0x00004400u}, @@ -151,7 +158,7 @@ void ExpectHalf(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, Expect(tid, actual, expected, name); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); @@ -159,7 +166,6 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -183,23 +189,37 @@ void Check() { } } +bool RefusesOutputModifier(AgcDriver::VulkanDevice& device, std::span code) { + try { + Run(device, code, ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find("output modifier on an f16") != std::string::npos; + } + return false; +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); + Run(*device, Code, std::nullopt); Check(); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Run(*device, Code, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); Check(); - bool refused = false; - try { - Run(*device, ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}); - } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("output modifier on an f16") != std::string::npos; + Require(RefusesOutputModifier(*device, Code), "float16 modifiers: an output modifier with f16 output denormals flushed was not refused"); + const std::pair, const char*> ternaries[] = { + {Min3Div2, "v_min3_f16 div:2"}, + {Max3Mul2, "v_max3_f16 mul:2"}, + {Med3Mul4, "v_med3_f16 mul:4"}, + {DivFixupMul2, "v_div_fixup_f16 mul:2"}, + }; + for (const auto& [code, name] : ternaries) { + Run(*device, code, std::nullopt); + Run(*device, code, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Require(RefusesOutputModifier(*device, code), std::string("float16 modifiers: ") + name + " with f16 output denormals flushed was not refused"); } - Require(refused, "float16 modifiers: an output modifier with f16 output denormals flushed was not refused"); std::puts("float16 modifiers tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FormattedStoreUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FormattedStoreUnaligned.cpp new file mode 100644 index 0000000000..a39cd01e81 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FormattedStoreUnaligned.cpp @@ -0,0 +1,135 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t LaneBytes = 64; +constexpr std::uint32_t Fill = 0xaaaaaaaau; + +struct Store { + std::uint32_t format; + std::uint32_t width; + std::uint32_t channels; + std::uint32_t components; + std::uint32_t offset; +}; + +constexpr std::array Stores{{ + {5, 1, 1, 1, 1}, + {11, 2, 1, 1, 3}, + {60, 1, 4, 4, 6}, + {60, 1, 4, 3, 14}, + {11, 2, 1, 1, 19}, + {27, 2, 2, 2, 26}, + {69, 2, 4, 4, 33}, + {18, 1, 2, 2, 45}, +}}; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::vector Program() { + std::vector code{0x34020084, 0x34060086}; + for (std::uint32_t component = 0; component < Inputs; ++component) code.insert(code.end(), {0xe0301000u | (component * 4u), 0x80000401u | (component << 8u)}); + code.push_back(0xbf8c3f70); + code.insert(code.end(), {0x7e3c02ff, Fill}); + for (std::uint32_t offset = 0; offset < LaneBytes; offset += 4) code.insert(code.end(), {0xe0701000u | offset, 0x80011e03}); + code.push_back(0xbf8c3f70); + for (const auto& store : Stores) { + const std::uint32_t op = 3u + store.components; + code.insert(code.end(), {0xe8001000u | (store.format << 19u) | (op << 16u) | store.offset, 0x80010403}); + } + code.push_back(0xbf810000); + return code; +} + +std::uint32_t Value(std::uint32_t tid, std::uint32_t component) { + const auto mixed = (tid + 1u) * 2654435761u + component * 40503u; + return tid % 5u == 3u ? mixed | 0x10000u : mixed >> 3u & 0xffffu; +} + +std::array Expected(std::uint32_t tid) { + std::array bytes{}; + bytes.fill(static_cast(Fill & 0xffu)); + for (const auto& store : Stores) { + const std::uint32_t width = store.width; + const std::uint32_t limit = width == 1u ? 0xffu : 0xffffu; + for (std::uint32_t component = 0; component < store.channels; ++component) { + const auto value = component < store.components ? std::min(Value(tid, component), limit) : 0u; + for (std::uint32_t byte = 0; byte < width; ++byte) bytes[store.offset + component * width + byte] = static_cast(value >> (8u * byte)); + } + } + 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}; +} + +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < Inputs; ++component) Input[tid * Inputs + component] = Value(tid, component); + } + Output.fill(0xdeu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + static const auto code = Program(); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 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(words.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = Expected(tid); + for (std::uint32_t byte = 0; byte < LaneBytes; ++byte) { + const auto actual = Output[tid * LaneBytes + byte]; + Require(actual == expected[byte], "formatted store unaligned: lane " + std::to_string(tid) + " byte " + std::to_string(byte) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[byte])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("formatted store unaligned 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/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp new file mode 100644 index 0000000000..f415d1e5e8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -0,0 +1,136 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); + +extern "C" { +std::int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject*, const char*, GuestFiberEntry, std::uint64_t, void*, std::uint64_t, const void*, std::uint32_t); +std::int32_t APS5_VABI sceFiberFinalize(FiberObject*); +std::int32_t APS5_VABI sceFiberRun_nid_postfix(FiberObject*, std::uint64_t, std::uint64_t*); +std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); +} + +namespace { + +using AgcDriver::DriverDetail::Driver; +using AgcDriver::Graphics::Require; + +constexpr std::size_t ContextBytes = 64 * 1024; +constexpr std::size_t MappingBytes = 4 * 65536; +constexpr std::size_t FiberStackLimit = 8 * 1024; +constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull; + +alignas(16) std::array FiberContext; +struct alignas(8) FiberStorage { + unsigned char bytes[0x100]; +}; +FiberStorage FiberObjectStorage; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; +alignas(8) Header VertexHeader{}; + +std::uint8_t* Mapping = nullptr; + +void RegisterVertexShader() { + VertexHeader.shader.file_header = 0x34333231u; + VertexHeader.shader.version = 0x18u; + VertexHeader.shader.header_size = sizeof(Header); + VertexHeader.shader.shader_size = static_cast(sizeof(VertexCode)); + VertexHeader.shader.code = VertexCode.data(); + VertexHeader.shader.type = 2; + VertexHeader.shader.user_data = &VertexHeader.userData; + AgcDriverRegisterShader_nid_postfix(&VertexHeader.shader); +} + +void MapForGpu() { + GuestAllocations::Mutation mutation; + mutation.Add(Mapping, MappingBytes, true, true, true); + mutation.NoteGpuMapping(Mapping, MappingBytes); +} + +void APS5_VABI MapOnFiber(std::uint64_t, std::uint64_t) { + MapForGpu(); + sceFiberReturnToThread(0, nullptr); +} + +void ReleaseMapping() { + if (Mapping == nullptr) return; +#ifdef _WIN32 + VirtualFree(Mapping, 0, MEM_RELEASE); +#else + std::free(Mapping); +#endif + Mapping = nullptr; +} + +std::size_t FiberStackUsed() { + std::size_t untouched = 0; + while (untouched < FiberContext.size() && FiberContext[untouched] == Fill) ++untouched; + return (FiberContext.size() - untouched) * sizeof(std::uint64_t); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegisterVertexShader(); +#ifdef _WIN32 + Mapping = static_cast(VirtualAlloc(nullptr, MappingBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + Mapping = static_cast(std::aligned_alloc(65536, MappingBytes)); +#endif + Require(Mapping != nullptr, "GPU mapping on a fiber: cannot allocate the mapped memory"); + std::memset(Mapping, 0, MappingBytes); + FiberContext.fill(Fill); + auto* fiber = reinterpret_cast(&FiberObjectStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "gpu map", MapOnFiber, 0, FiberContext.data(), ContextBytes, nullptr, 0) == 0, "GPU mapping on a fiber: the fiber was not initialized"); + const auto beforeFiber = Driver::GpuMapImportThreads(); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "GPU mapping on a fiber: the fiber did not run"); + const auto onFiber = Driver::GpuMapImportThreads() - beforeFiber; + Require(sceFiberFinalize(fiber) == 0, "GPU mapping on a fiber: the fiber was not finalized"); + const auto used = FiberStackUsed(); + GuestAllocations::Mutation().Remove(Mapping); + const auto beforeThread = Driver::GpuMapImportThreads(); + MapForGpu(); + const auto onThread = Driver::GpuMapImportThreads() - beforeThread; + GuestAllocations::Mutation().Remove(Mapping); + AgcDriverShutdown_nid_postfix(); + ReleaseMapping(); + Require(used <= FiberStackLimit, "GPU mapping on a fiber used " + std::to_string(used) + " bytes of the fiber's stack, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); +#ifdef _WIN32 + Require(onFiber == 0, "GPU mapping on a fiber started " + std::to_string(onFiber) + " host threads for its import instead of importing on the fiber's host stack"); +#else + Require(onFiber == 1, "GPU mapping on a fiber started " + std::to_string(onFiber) + " host threads for its import instead of one"); +#endif + Require(onThread == 0, "GPU mapping on an ordinary thread started " + std::to_string(onThread) + " host threads for its import instead of importing on its own stack"); + std::cout << "A GPU mapping made on a fiber used " << used << " bytes of its stack\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + ReleaseMapping(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp index c337c09d88..00a4f23f96 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp @@ -25,16 +25,18 @@ using AgcDriver::Graphics::Require; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t AllocationBytes = BlockBytes * 4; +constexpr std::size_t ReservationBytes = AllocationBytes + BlockBytes * 2; class GuestAllocation { public: GuestAllocation() { #ifdef _WIN32 - memory = static_cast(VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + allocation = static_cast(VirtualAlloc(nullptr, ReservationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); #else - memory = static_cast(std::aligned_alloc(BlockBytes, AllocationBytes)); + allocation = static_cast(std::aligned_alloc(BlockBytes, ReservationBytes)); #endif - Require(memory != nullptr, "host import at map: cannot allocate the guest memory"); + Require(allocation != nullptr, "host import at map: cannot allocate the guest memory"); + memory = allocation + BlockBytes; std::memset(memory, 0, AllocationBytes); Register(); } @@ -42,9 +44,9 @@ public: ~GuestAllocation() { if (registered) Unregister(); #ifdef _WIN32 - VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(allocation, 0, MEM_RELEASE); #else - std::free(memory); + std::free(allocation); #endif } @@ -73,6 +75,7 @@ public: } private: + std::uint8_t* allocation = nullptr; std::uint8_t* memory = nullptr; bool registered = false; }; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp new file mode 100644 index 0000000000..b44e5d2429 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleCubeDerivatives.cpp @@ -0,0 +1,163 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t ResultWord = 8; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Levels = 5; +constexpr std::uint32_t Faces = 6; +constexpr std::uint32_t Gradients = 4; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t TypeCube = 11; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xe030100c, + 0x80000801, 0xe0301010, 0x80000901, 0xe0301014, 0x80000a01, 0xe0301018, 0x80000b01, 0xbf8c3f70, + 0xf0880f18, 0x00610c05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +struct Sample { + std::uint32_t level; + std::uint32_t face; + std::uint32_t gradient; + std::uint32_t x; + std::uint32_t y; +}; + +Sample SampleOf(std::uint32_t tid) { + return {tid % Levels, (tid / Levels) % Faces, (tid / 3u) % Gradients, (tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size}; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((Levels - 1u) << 16u) | (TypeCube << 28u), + Faces - 1u, + (Levels - 1u) << 4u, + 0u, + 0u, + }; +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + auto* words = &Buffer[tid * Words]; + for (std::uint32_t gradient = 0; gradient < Gradients; ++gradient) { + words[gradient] = Bits(gradient == sample.gradient ? std::exp2(static_cast(sample.level) - 0.25f) / static_cast(Size) : 0.0f); + } + words[4] = Bits(1.0f + (static_cast(sample.x) + 0.5f) / static_cast(Size)); + words[5] = Bits(1.0f + (static_cast(sample.y) + 0.5f) / static_cast(Size)); + words[6] = Bits(static_cast(sample.face)); + } +} + +void FillTexture() { + Texels.fill(0xeeu); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Texels.data()))); + Require(surface.layers == Faces && surface.mips.size() == Levels && surface.guestBytes <= Texels.size(), "image sample cube derivatives: the cube mip chain does not fit the texel storage"); + for (std::uint32_t face = 0; face < Faces; ++face) { + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = surface.mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &Texels[surface.GuestLayerOffset(face) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(level); + texel[1] = static_cast(face); + texel[2] = 0u; + texel[3] = 255u; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data()); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array expected{sample.level, sample.face, 0u, 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + ResultWord + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), "image_sample_d cube: thread " + std::to_string(tid) + " (face " + std::to_string(sample.face) + ", level " + std::to_string(sample.level) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + FillTexture(); + Run(*device); + Check(); + std::puts("image sample cube derivatives 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/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp index 9a5394069b..88a589493d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -53,6 +53,12 @@ alignas(256) constexpr std::array LevelZeroCode{ 0x80000b01, 0xbf810000, }; +alignas(256) constexpr std::array GradientCode{ + 0x34020086, 0xe0301000, 0x80000601, 0xe0301004, 0x80000701, 0xbf8c3f70, 0x7e0402f6, 0x7e060280, + 0x7e080280, 0x7e0a02f6, 0xf0880f08, 0x00610802, 0xbf8c3f70, 0xe0701010, 0x80000801, 0xe0701014, + 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xbf810000, +}; + alignas(256) constexpr std::array OffsetCode{ 0x34020086, 0xe0301008, 0x80000201, 0xe0301000, 0x80000301, 0xe0301004, 0x80000401, 0xbf8c3f70, 0x7e0a02ff, 0x402ccccd, 0x7e2802ff, 0x0000023d, 0xf0dc0f08, 0x00610802, 0xf0d00f08, 0x00610c02, @@ -75,6 +81,7 @@ struct Program { const Program Plain{Code, {"image_sample_lz", "image_sample_l 2.7", "image_sample"}, {Offset::None, Offset::None, Offset::None}}; const Program LevelZero{LevelZeroCode, {"image_sample_lz", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; +const Program Gradient{GradientCode, {"image_sample_d (4, 0, 0, 4)", "", ""}, {Offset::None, Offset::None, Offset::None}, 1}; const Program Offsets{OffsetCode, {"image_sample_lz_o", "image_sample_l_o 2.7", "image_sample_lz_o (-3, 2) NSA"}, {Offset::Lane, Offset::Lane, Offset::Constant}}; alignas(256) constexpr std::array ConstantCode{ @@ -313,6 +320,8 @@ int main() { Check(Plain, sampler, 0u, "first level of a 4-level image", coordinates, multiSamples); Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image"); Check(Offsets, sampler, 0u, "1-level image", offsetCoordinates, Run(*device, Offsets, single, sampler.words, offsetCoordinates)); + Check(Gradient, sampler, 0u, "1-level image", coordinates, Run(*device, Gradient, single, sampler.words, coordinates)); + Check(Gradient, sampler, 0u, "first level of a 4-level image", coordinates, Run(*device, Gradient, multi, sampler.words, coordinates)); if (!sampler.atBaseLevel) continue; const auto levelZeroSingle = Run(*device, LevelZero, single, sampler.words, coordinates); const auto levelZeroChain = Run(*device, LevelZero, chain, sampler.words, coordinates); @@ -324,6 +333,7 @@ int main() { ExpectFailure(*device, Plain, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_l and image_sample"); ExpectFailure(*device, Plain, fromLevelOne, Samplers[0].words, "single-level", "a view starting at mip 1 read by image_sample_l and image_sample"); ExpectFailure(*device, Offsets, chain, Samplers[0].words, "single-level", "a 4-level view read with texel offsets"); + ExpectFailure(*device, Gradient, chain, Samplers[0].words, "single-level", "a 4-level view read by image_sample_d"); ExpectFailure(*device, LevelZero, chain, Samplers[2].words, "single-level", "a 4-level view through an S# with a minimum LOD and a LOD bias"); ExpectFailure(*device, Plain, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters"); ExpectFailure(*device, Plain, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp new file mode 100644 index 0000000000..8f419f2164 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -0,0 +1,295 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.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::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 48; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Slices = 4; +constexpr std::uint32_t FirstSlice = 1; +constexpr std::uint32_t Layers = Slices - FirstSlice; +constexpr std::uint32_t BandWidth = Width / Layers; +constexpr std::uint8_t Kept = 0x40; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe030201c, 0x80000405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +alignas(256) constexpr std::array GeometryCode{ + 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, 0xf80008d4, 0x00330000, 0xf800020f, + 0x33323130, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array LayerFieldPixelCode{ + 0x2c020090, 0x7e020d01, 0x100202f0, 0xf800180f, 0x01010101, 0xbf810000, +}; + +constexpr std::array WhiteBands{255, 255, 255}; +constexpr std::array LayerFieldBands{0, 128, 255}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +constexpr ShaderRecompiler::MeshConfiguration Subgroup{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void BuildBands() { + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const float left = -1.0f + 2.0f * static_cast(layer) / Layers; + const float right = -1.0f + 2.0f * static_cast(layer + 1u) / Layers; + const std::array, 6> corners{{{left, -1.0f}, {right, -1.0f}, {left, 1.0f}, {right, -1.0f}, {right, 1.0f}, {left, 1.0f}}}; + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) { + AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; + color.tileMode = ColorTileMode::RenderTarget; + color.depth = Slices; + color.depthSlice = FirstSlice; + color.layers = Layers; + color.bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)).guestBytes; + return color; +} + +class GuestVolume { +public: + explicit GuestVolume(std::size_t bytes) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, bytes)); +#endif + Require(block != nullptr, "cannot allocate the volume"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + std::memset(block, Kept, bytes); + } + + ~GuestVolume() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestVolume(const GuestVolume&) = delete; + GuestVolume& operator=(const GuestVolume&) = delete; + std::byte* Data() const { return block; } + +private: + std::byte* block = nullptr; +}; + +bool DeclaresLayer(const std::vector& words) { + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + if (length == 0 || at + length > words.size()) break; + if (opcode == 71u && length == 4 && words[at + 2] == 11u && words[at + 3] == 9u) return true; + if (opcode == 72u && length == 5 && words[at + 3] == 11u && words[at + 4] == 9u) return true; + at += length; + } + return false; +} + +void Run(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, bool mesh, std::span pixelCode, bool layerField) { + const auto target = device.Target(); + const auto vertexBuffer = BufferDescriptor(Vertices.data(), sizeof(Vertex), static_cast(Vertices.size())); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Vertices.size()), 0, 1, 0, false}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = LayerExport; + std::vector userData(mesh ? 12u : 4u, 0u); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), userData.begin() + (mesh ? 8 : 0)); + const std::span code = mesh ? std::span(GeometryCode) : std::span(VertexCode); + const auto stage = mesh ? ShaderStage::Mesh : ShaderStage::Vertex; + const std::array geometryMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest geometry{ + {stage, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, std::nullopt, vertexInfo, geometryMemory}, + target, + {0, 0, 0, mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : 64u} + }; + if (mesh) geometry.graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, Subgroup, std::nullopt, {draw.indexAddress, draw.indexCount, draw.indexSize, draw.instanceCount}}; + geometry.useCache = false; + const auto geometryResult = ShaderRecompiler::Recompile(geometry); + const auto geometryPush = static_cast(geometryResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + if (layerField) { + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.layerExported = true; + } + 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, {}}, + {64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(geometryPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {stage, &geometryResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(geometryPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + if (mesh) state.stages = {AgcDriver::Graphics::ShaderPath::Geometry, 0x20u, 64, 64, Subgroup, std::nullopt}; + else state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64, 64, std::nullopt, std::nullopt}; + state.color = color; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.renderLayers = Layers; + state.layerExports = LayerExport; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (mesh) { + Require(DeclaresLayer(geometryResult.spirv), "the mesh program declares no Layer output"); + VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + subgroup.subgroupSize = target.subgroupSize; + subgroup.supportedStages = VK_SHADER_STAGE_ALL; + subgroup.supportedOperations = 0xffu; + subgroup.quadOperationsInAllStages = VK_TRUE; + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, true); + return; + } + device.Draw(state, draw, shaders); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("layered volume draw test"); + device.WaitIdle(); +} + +std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12u) & (mask & 0xfff000u)) ^ ((z << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& color, const std::string& what, const std::array& bands) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); + Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices"); + const auto block = AgcDriver::Graphics::ThinBlockLayout(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4); + Require(Width <= block[1] && Height <= block[2], "the volume slices span several tile blocks"); + const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast(ColorTileMode::RenderTarget), 4); + Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation"); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const auto* base = volume + geometry.GuestLayerOffset(slice); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice; + const auto expected = drawn ? bands[slice - FirstSlice] : Kept; + const auto* texel = base + BlockOffset(*equation, x, y, slice); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = std::to_integer(texel[channel]); + Require(actual == expected || (drawn && expected == 128 && actual == 127), what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)")); + } + } + } + } +} + +} + +int main(int argc, char** argv) { + try { + const bool vertexOnly = argc == 2 && std::strcmp(argv[1], "--vertex-only") == 0; + Require(argc == 1 || vertexOnly, "usage: layered volume draw [--vertex-only]"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) == target.supportedCapabilities.end()) { + std::puts("skipped, the device has no VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + BuildBands(); + const GuestVolume volume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); + const auto color = VolumeTarget(volume.Data()); + Run(*device, color, false, PixelCode, false); + Check(volume.Data(), color, "vertex draw", WhiteBands); + const GuestVolume fieldVolume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u}); + const auto fieldColor = VolumeTarget(fieldVolume.Data()); + Run(*device, fieldColor, false, LayerFieldPixelCode, true); + Check(fieldVolume.Data(), fieldColor, "a pixel shader reading the render target index of its packed ancillary word", LayerFieldBands); + if (vertexOnly) std::puts("layered volume vertex draw tests passed"); + else if (target.mesh.has_value()) Run(*device, color, true, PixelCode, false); + else std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); + std::puts("layered volume draw tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp new file mode 100644 index 0000000000..7c66f54f0e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeSubmit.cpp @@ -0,0 +1,287 @@ +#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/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 48; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Slices = 4; +constexpr std::uint32_t FirstSlice = 1; +constexpr std::uint32_t Layers = Slices - FirstSlice; +constexpr std::uint32_t BandWidth = Width / Layers; +constexpr std::uint8_t Kept = 0x40; +constexpr std::uint32_t LayerExport = (1u << 18u) | (1u << 21u); +constexpr std::uint32_t ShaderViewportIndexLayerCapability = 5254; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; +constexpr std::uint32_t RenderTargetVolumeAttrib3 = (Slices - 1u) | (0x1bu << 14u) | (2u << 24u) | (1u << 27u); +constexpr std::size_t Block = 65536; + +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe030201c, 0x80020405, 0xbf8c3f70, 0xf80000cf, 0x03020100, 0xf80008d4, 0x00040000, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array color; + std::uint32_t layer; +}; + +alignas(256) std::array Vertices{}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31d, 0}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched volume"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return static_cast(block); +} + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +void BuildBands() { + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const float left = -1.0f + 2.0f * static_cast(layer) / Layers; + const float right = -1.0f + 2.0f * static_cast(layer + 1u) / Layers; + const std::array, 6> corners{{{left, -1.0f}, {right, -1.0f}, {left, 1.0f}, {right, -1.0f}, {right, 1.0f}, {left, 1.0f}}}; + for (std::uint32_t k = 0; k < corners.size(); ++k) Vertices[6u * layer + k] = {{corners[k][0], corners[k][1], 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f}, layer}; + } +} + +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) { + AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; + color.tileMode = ColorTileMode::RenderTarget; + color.elementBytes = 4; + color.depth = Slices; + color.depthSlice = FirstSlice; + color.layers = Layers; + color.bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)).guestBytes; + return color; +} + +std::vector Frame(const std::byte* volume, const std::byte* keys) { + const auto colorAddress = reinterpret_cast(volume); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, LayerExport); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x31b, FirstSlice | ((Slices - 1u) << 13u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x3b8, RenderTargetVolumeAttrib3); + if (keys != nullptr) { + const auto keyAddress = reinterpret_cast(keys); + SetRegister(words, SetContextReg, 0x31c, 0x28028u | 0x10000000u); + SetRegister(words, SetContextReg, 0x325, static_cast(keyAddress >> 8u)); + SetRegister(words, SetContextReg, 0x3a8, static_cast(keyAddress >> 40u)); + } + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(Vertices.data(), sizeof(Vertex), static_cast(Vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(Vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < equation.bits.size(); ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12u) & (mask & 0xfff000u)) ^ ((z << 24u) & (mask & 0xff000000u)); + offset |= static_cast(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +void Check(const std::vector& volume, const AgcDriver::Graphics::ColorTarget& color, std::uint8_t undrawn, const std::string& what) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); + Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices"); + const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast(ColorTileMode::RenderTarget), 4); + Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation"); + for (std::uint32_t slice = 0; slice < Slices; ++slice) { + const auto* base = volume.data() + geometry.GuestLayerOffset(slice); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice; + const auto expected = drawn ? std::uint8_t{255} : undrawn; + const auto* texel = base + BlockOffset(*equation, x, y, slice); + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = std::to_integer(texel[channel]); + Require(actual == expected, what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)")); + } + } + } + } +} + +bool DeviceRoutesLayers() { + const auto device = OpenVulkanTestDevice(); + if (!device) return false; + const auto target = device->Target(); + return std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), ShaderViewportIndexLayerCapability) != target.supportedCapabilities.end(); +} + +void Run(const char* what, std::optional key) { + const auto bytes = (VolumeTarget(nullptr).bytes + Block - 1u) & ~(Block - 1u); + auto* volume = AllocateWatched(bytes); + std::memset(volume, static_cast(Kept), bytes); + const auto color = VolumeTarget(volume); + const auto keyBytes = static_cast(color.bytes / 256u); + std::byte* keys = nullptr; + if (key) { + keys = AllocateWatched(Block); + std::memset(keys, *key, keyBytes); + } + EndOfPipe = 0; + auto words = Frame(volume, keys); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string(what) + ": the DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string(what) + ": the end-of-pipe label was not written"); + std::vector stored(bytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(volume)), stored); + Check(stored, color, key == std::uint8_t{0x00} ? std::uint8_t{0} : Kept, what); + if (keys != nullptr) { + std::vector storedKeys(keyBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(keys)), storedKeys); + const auto uncompressed = static_cast(std::count(storedKeys.begin(), storedKeys.end(), std::byte{0xff})); + Require(uncompressed == keyBytes, std::string(what) + ": " + std::to_string(uncompressed) + " of " + std::to_string(keyBytes) + " DCC key bytes read uncompressed after the draw"); + } +} + +} + +int main() { + try { + if (!DeviceRoutesLayers()) { + std::puts("skipped, no Vulkan device with VK_EXT_shader_viewport_index_layer"); + return VulkanTestSkipped; + } + BuildBands(); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + Run("layered volume draw through the driver", std::nullopt); + Run("layered volume draw through the driver under uncompressed DCC keys", std::uint8_t{0xff}); + Run("layered volume draw through the driver under 0000 fast-clear DCC keys", std::uint8_t{0x00}); + AgcDriverShutdown_nid_postfix(); + std::puts("layered volume draws through the driver 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/PixelAncillaryWord.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp new file mode 100644 index 0000000000..559a514ed7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelAncillaryWord.cpp @@ -0,0 +1,306 @@ +#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/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 Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 5> Programs{}; +std::size_t programCount = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::uint32_t ancillaryVgpr, bool wholeWord) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + if (wholeWord) { + code[at++] = 0x36000000u | (20u << 17u) | (ancillaryVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (ancillaryVgpr << 9u) | 0x90u; + } else { + code[at++] = 0xd5480000u | 20u; + code[at++] = (0x84u << 18u) | (0x88u << 9u) | (256u + ancillaryVgpr); + code[at++] = 0x7e000280u | (21u << 17u); + } + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = 0x7e000280u | (22u << 17u); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + 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); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, ShaderRecompiler::RasterPrimitive primitive, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +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); + AgcDriver::GuestMemory::Read(address, std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t inputAddr, std::uint32_t ancillaryVgpr, ShaderRecompiler::RasterPrimitive primitive, const std::string& what) { + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, 16).Bytes() == bytes, what + ": the linear target is padded"); + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.format == VK_FORMAT_R32G32B32A32_SFLOAT && color.elementBytes == 16 && color.bytes == bytes, what + ": the target decoded to the wrong format"); + Draw(device, color, inputAddr, primitive, Program(ancillaryVgpr, true)); + const auto stored = ReadBack(device, block.Address(), bytes); + const auto word = static_cast(primitive); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(word & 0xffffu), static_cast(word >> 16u), 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +std::uint64_t PixelModule(const ShaderRecompiler::SpirvTarget& target, std::span code, ShaderRecompiler::RasterPrimitive primitive) { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + pixel.ancillary = true; + pixel.rasterPrimitive = primitive; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, std::nullopt, pixel, std::nullopt, memory}, + target, + PixelPushLayout(0, target) + }; + return ShaderRecompiler::Recompile(request).PipelineVariantId(); +} + +void ExpectModules(const ShaderRecompiler::SpirvTarget& target, bool wholeWord, const std::string& what) { + using ShaderRecompiler::RasterPrimitive; + const auto code = Program(0u, wholeWord); + const auto triangle = PixelModule(target, code, RasterPrimitive::Triangle); + Require(PixelModule(target, code, RasterPrimitive::Triangle) == triangle, what + ": the same primitive class built a second module"); + for (const auto primitive : {RasterPrimitive::Line, RasterPrimitive::Rectangle}) { + Require((PixelModule(target, code, primitive) != triangle) == wholeWord, what + (wholeWord ? ": a line or rectangle draw reused the triangle module" : ": a line or rectangle draw built its own module")); + } +} + +void ExpectPrimitive(VkPrimitiveTopology topology, bool rectList, bool tessellated, ShaderRecompiler::RasterPrimitive expected, const std::string& what) { + AgcDriver::Graphics::State state{}; + state.topology = topology; + state.rectList = rectList; + if (tessellated) state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{}; + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == expected, what + ": wrong primitive class"); +} + +} + +int main() { + try { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + using ShaderRecompiler::RasterPrimitive; + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, false, false, RasterPrimitive::Triangle, "triangle strip"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_LINE_LIST, false, false, RasterPrimitive::Line, "line list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, false, false, RasterPrimitive::Point, "point list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, true, false, RasterPrimitive::Rectangle, "rect list"); + ExpectPrimitive(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, false, true, RasterPrimitive::Triangle, "tessellated patches"); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Triangle, "ANCILLARY alone, triangle"); + Expect(*device, PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::Ancillary), 2u, RasterPrimitive::Rectangle, "ANCILLARY after the perspective center, rectangle"); + Expect(*device, PixelInputBit(PixelInput::Ancillary), 0u, RasterPrimitive::Line, "ANCILLARY alone, line"); + ExpectModules(device->Target(), true, "whole word"); + ExpectModules(device->Target(), false, "sample field only"); + std::puts("pixel ancillary word 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/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index d96b7a9ec3..d0a96ffef6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -4,13 +4,16 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include #include #include #include #include #include +#include #include +#include #include namespace { @@ -300,6 +303,41 @@ void TransactionLock() { Require(excluded, "a transaction ran beside one that had read prepared state"); } +void PreparationPriority(bool beforeSubmit) { + const auto workers = std::max(2u, std::thread::hardware_concurrency() / 2u); + std::barrier started(static_cast(workers + 1u)); + std::counting_semaphore<> release(0); + std::latch finished(static_cast(workers)); + for (unsigned index = 0; index < workers; ++index) { + SubmitRegistrationPreparation([&] { + started.arrive_and_wait(); + release.acquire(); + finished.count_down(); + }); + } + started.arrive_and_wait(); + PreparedShaders prepared; + std::atomic order{0}; + std::promise background; + std::promise demanded; + auto backgroundDone = background.get_future(); + auto demandedDone = demanded.get_future(); + SubmitRegistrationPreparation([&] { background.set_value(order.fetch_add(1u)); }); + if (beforeSubmit) PrioritizeRegistrationPreparation(prepared); + SubmitRegistrationPreparation([&] { demanded.set_value(order.fetch_add(1u)); }, &prepared); + if (!beforeSubmit) { + PrioritizeRegistrationPreparation(prepared); + PrioritizeRegistrationPreparation(prepared); + } + release.release(); + const bool settled = demandedDone.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + const bool backgroundSettled = backgroundDone.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + release.release(static_cast(workers - 1u)); + finished.wait(); + Require(settled && backgroundSettled, "preparation queue did not complete its work"); + Require(demandedDone.get() == 0u && backgroundDone.get() == 1u && order.load() == 2u, "demanded preparation waited behind background work or ran twice"); +} + } int main() { @@ -308,6 +346,8 @@ int main() { HeaderSelection(); FusedHullHeaderSelection(); TransactionLock(); + PreparationPriority(false); + PreparationPriority(true); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Registration(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index e34258828d..d1b66658cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -477,10 +477,12 @@ void Registration(bool indirect) { Shader copy = header.shader; copy.user_data = reinterpret_cast(©); AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); - alignas(Shader) std::array padded{}; - std::memcpy(padded.data(), ©, sizeof(Shader)); + alignas(Shader) std::array padded{}; + auto* unaligned = padded.data() + 4; + Require(reinterpret_cast(unaligned) % alignof(Shader) != 0, "shader ABI test header is unexpectedly aligned"); + std::memcpy(unaligned, ©, sizeof(Shader)); padded.back() = std::byte{0x7d}; - AgcDriverResolveShaderAbi_nid_postfix(reinterpret_cast(padded.data()), {}, {}); + AgcDriverResolveShaderAbi_nid_postfix(reinterpret_cast(unaligned), {}, {}); copy.target ^= 1u; ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); }, "replaced shader header"); header.registers[1].value |= 0x100u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp index 98d2d1b894..86fc0c75af 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeAncillary.cpp @@ -135,9 +135,10 @@ int main() { Draw(*device, code, 1u, waveSize, modifier == 0xff00e402u ? std::byte{255} : std::byte{0}); Draw(*device, code, 8u, waveSize, std::byte{0}); } - const std::array unsupported{0xbefc0300u, 0x7e028700u, 0xf800180fu, 0x01010101u, 0xbf810000u}; - Draw(*device, unsupported, 0u, waveSize, std::byte{0}, true); - Draw(*device, unsupported, 4u, waveSize, std::byte{0}, true, true); + const std::array whole{0xbefc0300u, 0x7e028700u, 0x7e020d01u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + std::cerr << "whole word\n"; + Draw(*device, whole, 0u, waveSize, std::byte{255}); + Draw(*device, whole, 4u, waveSize, std::byte{255}, false, true); } std::cout << "relative ancillary GPU readback tests passed\n"; return 0; 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/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp new file mode 100644 index 0000000000..add6ad6737 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -0,0 +1,396 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "VulkanTestDevice.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 + +namespace { + +using namespace AgcDriver::Graphics; + +constexpr std::size_t PageBytes = 65536; +constexpr std::size_t BufferBytes = 3 * PageBytes; +constexpr std::size_t ReservedBytes = 4 * PageBytes; +constexpr std::uint32_t WorkgroupSize = 64; +constexpr std::array Patterns{0x11223344u, 0x8899aabbu, 0x55667788u}; + +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"); + const auto properties = function("vkGetPhysicalDeviceProperties"); + int rank = -1; + for (const auto candidate : devices) { + VkPhysicalDeviceProperties described{}; + properties(candidate, &described); + if (described.apiVersion < VK_API_VERSION_1_1) continue; + const int candidateRank = described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? 2 : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU ? 1 : 0; + if (candidateRank <= rank) continue; + context.physical = candidate; + rank = candidateRank; + } + Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + VkPhysicalDeviceProperties2 selected{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &selected); + context.limits = selected.properties.limits; + 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"); + std::printf("Selected GPU name=%s vendor=0x%x device=0x%x subgroup=%u\n", selected.properties.deviceName, selected.properties.vendorID, selected.properties.deviceID, subgroup.subgroupSize); + std::printf("sparse buffer holes GPU: api=%u.%u.%u; queueFamily=%u\n", VK_API_VERSION_MAJOR(selected.properties.apiVersion), VK_API_VERSION_MINOR(selected.properties.apiVersion), VK_API_VERSION_PATCH(selected.properties.apiVersion), family); + 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); + 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.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 GuestPages { +public: + explicit GuestPages(bool committed) { +#ifdef _WIN32 + memory = static_cast(VirtualAlloc(nullptr, ReservedBytes, MEM_RESERVE, PAGE_NOACCESS)); +#else + const auto block = mmap(nullptr, ReservedBytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (block != MAP_FAILED) memory = static_cast(block); +#endif + Require(memory != nullptr, "cannot reserve sparse guest pages"); + try { + for (std::size_t page = 0; page < Patterns.size(); ++page) { + if (page == 1 && !committed) continue; + auto* begin = memory + page * PageBytes; +#ifdef _WIN32 + Require(VirtualAlloc(begin, PageBytes, MEM_COMMIT, PAGE_READWRITE) == begin, "cannot commit sparse guest page"); +#else + Require(mprotect(begin, PageBytes, PROT_READ | PROT_WRITE) == 0, "cannot commit sparse guest page"); +#endif + for (std::size_t offset = 0; offset < PageBytes; offset += sizeof(std::uint32_t)) std::memcpy(begin + offset, &Patterns[page], sizeof(std::uint32_t)); + } + } catch (...) { + release(); + throw; + } + } + + ~GuestPages() { release(); } + GuestPages(const GuestPages&) = delete; + GuestPages& operator=(const GuestPages&) = delete; + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + void release() noexcept { +#ifdef _WIN32 + if (memory != nullptr) VirtualFree(memory, 0, MEM_RELEASE); +#else + if (memory != nullptr) munmap(memory, ReservedBytes); +#endif + memory = nullptr; + } + + std::byte* memory = nullptr; +}; + +void Emit(std::vector& words, spv::Op opcode, std::initializer_list operands) { + words.push_back(static_cast((operands.size() + 1u) << 16u) | opcode); + words.insert(words.end(), operands); +} + +std::vector CopyModule() { + enum : std::uint32_t { Void = 1, FunctionType, Uint, Uint3, Words, Block, BlockPointer, Source, Destination, UintPointer, InputPointer, Invocation, Zero, Main, Label, InvocationValue, Index, SourcePointer, Value, DestinationPointer, Bound }; + std::vector words{spv::MagicNumber, 0x00010300u, 0u, Bound, 0u}; + Emit(words, spv::OpCapability, {spv::CapabilityShader}); + Emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450}); + Emit(words, spv::OpEntryPoint, {spv::ExecutionModelGLCompute, Main, 0x6e69616du, 0u, Invocation}); + Emit(words, spv::OpExecutionMode, {Main, spv::ExecutionModeLocalSize, WorkgroupSize, 1, 1}); + Emit(words, spv::OpDecorate, {Invocation, spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId}); + Emit(words, spv::OpDecorate, {Words, spv::DecorationArrayStride, 4}); + Emit(words, spv::OpDecorate, {Block, spv::DecorationBlock}); + Emit(words, spv::OpMemberDecorate, {Block, 0, spv::DecorationOffset, 0}); + Emit(words, spv::OpDecorate, {Source, spv::DecorationDescriptorSet, 0}); + Emit(words, spv::OpDecorate, {Source, spv::DecorationBinding, 0}); + Emit(words, spv::OpDecorate, {Destination, spv::DecorationDescriptorSet, 0}); + Emit(words, spv::OpDecorate, {Destination, spv::DecorationBinding, 1}); + Emit(words, spv::OpTypeVoid, {Void}); + Emit(words, spv::OpTypeFunction, {FunctionType, Void}); + Emit(words, spv::OpTypeInt, {Uint, 32, 0}); + Emit(words, spv::OpTypeVector, {Uint3, Uint, 3}); + Emit(words, spv::OpTypeRuntimeArray, {Words, Uint}); + Emit(words, spv::OpTypeStruct, {Block, Words}); + Emit(words, spv::OpTypePointer, {BlockPointer, spv::StorageClassStorageBuffer, Block}); + Emit(words, spv::OpVariable, {BlockPointer, Source, spv::StorageClassStorageBuffer}); + Emit(words, spv::OpVariable, {BlockPointer, Destination, spv::StorageClassStorageBuffer}); + Emit(words, spv::OpTypePointer, {UintPointer, spv::StorageClassStorageBuffer, Uint}); + Emit(words, spv::OpTypePointer, {InputPointer, spv::StorageClassInput, Uint3}); + Emit(words, spv::OpVariable, {InputPointer, Invocation, spv::StorageClassInput}); + Emit(words, spv::OpConstant, {Uint, Zero, 0}); + Emit(words, spv::OpFunction, {Void, Main, 0, FunctionType}); + Emit(words, spv::OpLabel, {Label}); + Emit(words, spv::OpLoad, {Uint3, InvocationValue, Invocation}); + Emit(words, spv::OpCompositeExtract, {Uint, Index, InvocationValue, 0}); + Emit(words, spv::OpAccessChain, {UintPointer, SourcePointer, Source, Zero, Index}); + Emit(words, spv::OpLoad, {Uint, Value, SourcePointer}); + Emit(words, spv::OpAccessChain, {UintPointer, DestinationPointer, Destination, Zero, Index}); + Emit(words, spv::OpStore, {DestinationPointer, Value}); + Emit(words, spv::OpReturn, {}); + Emit(words, spv::OpFunctionEnd, {}); + return words; +} + +class CopyPipeline { +public: + explicit CopyPipeline(const Context& context) : context(context) { + try { + const std::array bindings{{{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}, {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}}}; + VkDescriptorSetLayoutCreateInfo setLayoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + setLayoutInfo.bindingCount = static_cast(bindings.size()); + setLayoutInfo.pBindings = bindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &setLayoutInfo, nullptr, &setLayout), "vkCreateDescriptorSetLayout"); + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &descriptorPool), "vkCreateDescriptorPool"); + VkDescriptorSetAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocateInfo.descriptorPool = descriptorPool; + allocateInfo.descriptorSetCount = 1; + allocateInfo.pSetLayouts = &setLayout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocateInfo, &set), "vkAllocateDescriptorSets"); + const auto module = CopyModule(); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = module.size() * sizeof(std::uint32_t); + moduleInfo.pCode = module.data(); + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &shader), "vkCreateShaderModule"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &setLayout; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); + VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + pipelineInfo.stage.module = shader; + pipelineInfo.stage.pName = "main"; + pipelineInfo.layout = pipelineLayout; + Check(context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateComputePipelines"); + } catch (...) { + release(); + throw; + } + } + + ~CopyPipeline() { release(); } + CopyPipeline(const CopyPipeline&) = delete; + CopyPipeline& operator=(const CopyPipeline&) = delete; + + void Copy(const VkDescriptorBufferInfo& source, Buffer& destination) const { + Require(source.range == BufferBytes && destination.Bytes().size() == BufferBytes, "sparse buffer holes GPU copy has an unexpected extent"); + const std::array infos{{source, {destination.Handle(), 0, BufferBytes}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[binding].pBufferInfo = &infos[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + CommandBatch batch(context); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + context.Function("vkCmdBindPipeline")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Function("vkCmdBindDescriptorSets")(batch.Handle(), VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); + context.Function("vkCmdDispatch")(batch.Handle(), static_cast(BufferBytes / sizeof(std::uint32_t) / WorkgroupSize), 1, 1); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + destination.Invalidate(); + } + +private: + void release() noexcept { + if (pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (pipelineLayout != VK_NULL_HANDLE) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (shader != VK_NULL_HANDLE) context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + if (descriptorPool != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorPool")(context.device, descriptorPool, nullptr); + if (setLayout != VK_NULL_HANDLE) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + + const Context& context; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkShaderModule shader = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +void Run(const Context& context, const CopyPipeline& pipeline, bool writable, const std::string& shape) { + const auto label = std::string("sparse buffer holes GPU: ") + (writable ? "writable " : "readable ") + shape; + const bool committed = shape == "committed"; + const auto requested = shape == "shortened" ? ReservedBytes : BufferBytes; + GuestPages pages(committed); + const auto ranges = AgcDriver::GuestMemory::DescribeCommitted(pages.Address(), requested); + Require(ranges.whole == committed, label + ": unexpected guest commitment"); + if (!committed) { + Require(ranges.ranges == std::vector>{{pages.Address(), pages.Address() + PageBytes}, {pages.Address() + 2 * PageBytes, pages.Address() + BufferBytes}}, label + ": guest hole is not inaccessible"); + } + Buffer readback(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0xcc, BufferBytes); + VkBuffer primed = VK_NULL_HANDLE; + { + Buffer earlier(context, BufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + primed = earlier.Handle(); + std::memset(earlier.Bytes().data(), 0xa5, BufferBytes); + } + GuestBufferMemory memory(context); + if (writable) memory.AddWritable(pages.Address(), requested); + else memory.AddReadable(pages.Address(), requested); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + memory.Upload(false); + } + std::uint32_t adjustment = 0; + const auto descriptor = memory.Descriptor(pages.Address(), requested, adjustment); + Require(descriptor.buffer == primed && descriptor.offset == 0 && descriptor.range == BufferBytes && adjustment == 0, label + ": the upload did not reuse the primed buffer at the expected extent"); + std::cout << label << " reused primed buffer; requested=" << requested << "; allocated=" << descriptor.range << '\n'; + pipeline.Copy(descriptor, readback); + const auto bytes = readback.Bytes(); + std::size_t mismatches = 0; + std::size_t staleWords = 0; + for (std::size_t offset = 0; offset < BufferBytes; offset += sizeof(std::uint32_t)) { + std::uint32_t actual = 0; + std::memcpy(&actual, bytes.data() + offset, sizeof(actual)); + const auto page = offset / PageBytes; + if (page == 1 && !committed) { + mismatches += actual != 0; + staleWords += actual == 0xa5a5a5a5u; + } else { + Require(actual == Patterns[page], label + ": GPU readback changed backed guest bytes"); + } + } + std::cout << label << " readback checked; words=" << BufferBytes / sizeof(std::uint32_t) << "; nonzeroHoleWords=" << mismatches << "; staleHoleWords=" << staleWords << '\n'; + Require(mismatches == 0, label + ": GPU readback exposed stale pooled bytes in guest holes"); + std::cout << label << " passed\n"; +} + +} + +int main() { + try { + 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; + } + const auto& context = device->GetContext(); + Require(context.limits.maxStorageBufferRange >= BufferBytes && context.limits.maxComputeWorkGroupSize[0] >= WorkgroupSize && context.limits.maxComputeWorkGroupInvocations >= WorkgroupSize && context.limits.maxComputeWorkGroupCount[0] >= BufferBytes / sizeof(std::uint32_t) / WorkgroupSize, "Vulkan limits cannot support the bounded sparse buffer readback"); + CopyPipeline pipeline(context); + bool passed = true; + for (const bool writable : {false, true}) { + for (const auto* shape : {"interior", "shortened", "committed"}) { + try { + Run(context, pipeline, writable, shape); + } catch (const std::exception& error) { + if (std::string(error.what()).find(": GPU readback exposed stale pooled bytes in guest holes") == std::string::npos) throw; + std::cerr << error.what() << '\n'; + passed = false; + } + } + } + if (!passed) return 1; + std::puts("Sparse buffer hole GPU readback 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/StoreIdleResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp index cec60574e9..b2935dd9c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include #include @@ -75,18 +78,28 @@ void Store(const std::uint32_t* texels) { } -int main() { +int main(int argc, char** argv) { try { { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; } std::vector storage((BlockBytes + 0x10000u) / 4u); + const bool wake = argc == 2 && std::string_view(argv[1]) == "--idle-epoch-wake"; + std::vector nextStorage(wake ? (BlockBytes + 0x10000u) / 4u : 0u); + auto* nextTexels = wake ? reinterpret_cast((reinterpret_cast(nextStorage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}) : nullptr; + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); std::fill(texels, texels + BlockBytes / 4u, Untouched); { GuestAllocations::Mutation mutation; mutation.Add(texels, BlockBytes, true, true, true); + if (nextTexels != nullptr) { + std::fill(nextTexels, nextTexels + BlockBytes / 4u, Untouched); + mutation.Add(nextTexels, BlockBytes, true, true, true); + } } const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; const volatile std::uint32_t* cpu = texels; @@ -98,6 +111,22 @@ int main() { }; Store(texels); + if (wake) { + const auto pendingDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!AgcDriver::Graphics::StorageTexture::AnyPending() && std::chrono::steady_clock::now() < pendingDeadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(AgcDriver::Graphics::StorageTexture::AnyPending(), "idle wake fixture did not observe pending storage results"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + { + std::lock_guard gpuLock(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::StorageTexture::FlushAllPending("idle wake fixture"); + } + std::array nop{0xc0001000u, 0u}; + Packet packet{nop.data(), static_cast(nop.size()), 0, {}}; + AgcDriver::Submit(&packet, 0x30u); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + cpu = nextTexels; + Store(nextTexels); + } const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(stored(), "the queue-idle store did not bring the result to guest memory without a direct flush or further submission"); @@ -106,6 +135,7 @@ int main() { { GuestAllocations::Mutation mutation; mutation.Remove(texels); + if (nextTexels != nullptr) mutation.Remove(nextTexels); } std::puts("store idle results tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp new file mode 100644 index 0000000000..3caabc9f92 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -0,0 +1,328 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.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 "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +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 Page = 0x1000; +constexpr std::size_t Block = 0x10000; +constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t Filled = 0x9abcdef0u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + 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), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(std::span code, std::uint32_t threads, std::uint32_t resource, std::span userData, std::uint32_t groups) { + std::vector words; + const auto registers = [&](const std::uint32_t first, const 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{threads, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{resource}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +void Store(const std::uint32_t* texels) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + Dispatch(StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); +} + +void Fill(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + const std::array userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + Filled, + Filled, + Filled, + Filled, + }; + Dispatch(FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); +} + +class Surface { +public: + explicit Surface(bool watched = false) : watched(watched) { + if (watched) { +#ifdef _WIN32 + texels = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); + texels = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes); + } +#endif + } else { + storage.resize((SurfaceBytes + Block) / 4u); + texels = reinterpret_cast((reinterpret_cast(storage.data()) + Block - 1) & ~std::uintptr_t{Block - 1}); + } + Require(texels != nullptr, "store over untracked: cannot allocate the guest surface"); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true); + } + + ~Surface() { + GuestAllocations::Mutation().Remove(texels); + if (!watched) return; +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes); +#else + munmap(texels, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes); +#endif + } + + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + std::uint32_t* Texels() const { return texels; } + std::uint8_t* Bytes() const { return reinterpret_cast(texels); } + std::uint64_t Address() const { return AddressOf(texels); } + +private: + bool watched; + std::vector storage; + std::uint32_t* texels = nullptr; +}; + +std::uint8_t Pattern(std::size_t offset) { + return static_cast(0x40u + offset % 0x3bu); +} + +std::size_t LostBytes(const std::uint8_t* bytes) { + std::size_t lost = 0; + for (std::size_t offset = 0; offset < Page; ++offset) { + if (static_cast(bytes)[offset] != Pattern(offset)) ++lost; + } + return lost; +} + +bool Holds(const std::uint32_t* texels, std::uint32_t value) { + const auto* words = static_cast(texels); + for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) { + if (words[index] == value) return true; + } + return false; +} + +template +bool HoldPending(THold& hold, const Surface& surface, std::uint32_t result) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + if (StorageTexture::AnyPending()) return true; + hold.unlock(); + if (Holds(surface.Texels(), result)) return false; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the result neither became pending nor reached guest memory"); + std::this_thread::yield(); + } +} + +template +void Settle(THold& hold, const Surface& surface, std::uint32_t result) { + if (!HoldPending(hold, surface, result)) return; + StorageTexture::FlushAllPending("store over untracked fixture"); + hold.unlock(); +} + +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch }; + +struct Case { + const char* name; + Kind kind; + std::size_t access; +}; + +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); + 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); + Fill(source->Texels()); + Settle(hold, *source, Filled); + } + Store(surface.Texels()); + if (test.kind == Kind::Filled) { + Settle(hold, surface, Stored); + Fill(surface.Texels()); + if (!HoldPending(hold, surface, Filled)) return Outcome::Missed; + } else if (test.kind == Kind::Copied) { + Settle(hold, surface, Stored); + hold.lock(); + const auto from = StorageTexture::FindLive(source->Address(), SurfaceBytes); + const auto to = StorageTexture::FindLive(address, SurfaceBytes); + Require(from != nullptr && to != nullptr, "store over untracked: the surfaces 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"); + std::fprintf(stderr, "[store over untracked] %s: not run, the surface has no host import\n", test.name); + return Outcome::Kept; + } + } else if (!HoldPending(hold, surface, Stored)) { + return Outcome::Missed; + } + if (test.kind == Kind::LeftWatch) { + 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); + return Outcome::Kept; + } + 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); + } + 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; + } + 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 (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; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + { + Surface probe; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); + } + 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}, + }}; + bool kept = true; + for (const auto& test : cases) { + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 256u && outcome == Outcome::Missed; ++attempt) outcome = Run(test); + Require(outcome != Outcome::Missed, "store over untracked: the result was never held pending"); + kept = outcome == Outcome::Kept && kept; + } + Require(kept, "a pending storage result was stored over bytes the CPU wrote to untracked memory"); + AgcDriverShutdown_nid_postfix(); + std::puts("store over untracked 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/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index 0e991c3071..925da125e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -64,6 +64,26 @@ alignas(256) constexpr std::array LocalIndexAsData{ 0x0000030a, 0xd8340010, 0x0000070a, 0xbf8cc07f, 0xbefd2106, }; +alignas(256) constexpr std::array HullDescendingRecords{ + 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, 0xbf810000, +}; + +alignas(256) constexpr std::array HullPatchConstant{ + 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, 0xe0701064, 0x0202060b, 0xbf810000, +}; + alignas(256) constexpr std::array HullUnguardedFactors{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -73,6 +93,26 @@ alignas(256) constexpr std::array HullUnguardedFactors{ 0x16161690, 0x4c1616ff, 0x00008000, 0xe0741000, 0x0202070b, 0xbf810000, }; +alignas(256) constexpr std::array HullDynamicIndex{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x26002882, 0x34000085, 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 HullHalfSlotIndex{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x26002882, 0x34000084, 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 HullSquaredAddress{ 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, 0x02151101, 0x362402ff, 0x000000ff, 0x16002914, 0xd5430015, 0x040224ff, 0x00000060, 0xdbfc0000, @@ -92,12 +132,39 @@ alignas(256) constexpr std::array HullAndSaveexec{ 0xbf810000, }; -alignas(256) constexpr std::array DomainPatchRead{ +alignas(256) constexpr std::array DomainPatchRead{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, - 0xe0341020, 0x04030e01, 0x4a060e81, 0x16060690, 0x4c0606ff, 0x00008000, 0xe0341000, 0x04030c03, - 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, - 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, - 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x102a2aff, 0x3e000000, + 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainNeighbourRecord{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0301134, 0x04031603, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainFixedPatchRead{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0300064, 0x04031600, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, +}; + +alignas(256) constexpr std::array DomainNeighbourPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, + 0xe0341020, 0x04030e01, 0x4a060e81, 0x160606ff, 0x000000d0, 0x4c0606ff, 0x00008000, 0xe0301064, + 0x04031503, 0xe0301030, 0x04031601, 0xbf8c3f70, 0x062a2b16, 0x08040af2, 0x08040d02, 0x10201505, + 0x3e201906, 0x3e201d02, 0x10221705, 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, + 0x40400000, 0x102a2aff, 0x3e000000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, + 0x14151214, 0xbf810000, }; alignas(256) constexpr std::array DomainCulledPrimitive{ @@ -107,11 +174,30 @@ alignas(256) constexpr std::array DomainCulledPrimitive{ 0x40400000, 0x7e260280, 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array QuadHullCode{ + 0xd7650000, 0x000100c1, 0xd7660000, 0x000200c1, 0xbf8a0000, 0x7da206f9, 0x81060000, 0xd5480014, + 0x02151101, 0x362402ff, 0x000000ff, 0x160028a0, 0xd5430015, 0x040224ff, 0x00000080, 0xdbfc0000, + 0x02000015, 0x162c2890, 0xd5430016, 0x045a24ff, 0x00000040, 0xbf8cc07f, 0xe0781000, 0x02020216, + 0x7da82881, 0xb06a0080, 0x162602f9, 0x0086066a, 0xd8d80008, 0x06000013, 0xbf8cc07f, 0x7e300306, + 0x7e320306, 0x7e340306, 0x7e360306, 0x16142498, 0xe0781000, 0x0403180a, 0xe0741010, 0x0403180a, + 0xbf810000, +}; + +alignas(256) constexpr std::array QuadDomainCode{ + 0xf8000941, 0x00000000, 0x34020e86, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, + 0x04030e01, 0xe0341030, 0x04031001, 0xbf8c3f70, 0x08040af2, 0x08060cf2, 0x10241502, 0x3e241905, + 0x10261702, 0x3e261b05, 0x10281d05, 0x3e282102, 0x102a1f05, 0x3e2a2302, 0x102c2503, 0x3e2c2906, + 0x102e2703, 0x3e2e2b06, 0xd5510018, 0x040a0d05, 0xd5510018, 0x04620718, 0x103030f4, 0x7e320280, + 0x7e3402f2, 0xf80008cf, 0x1a191716, 0xf800020f, 0x1a19181a, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; -constexpr ShaderRecompiler::TessellationConfiguration Patches{3u, 3u, 1u, 2u, 2u}; +using ShaderRecompiler::TessellationConfiguration; + +constexpr TessellationConfiguration Patches{3u, 3u, 1u, 2u, 2u}; constexpr std::array PatchLevels{1u, 0u, 5u}; constexpr std::uint16_t Corner(std::uint32_t patch, std::uint32_t x, std::uint32_t y) { @@ -124,6 +210,21 @@ alignas(256) constexpr std::array Indices{ Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 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), + Corner(2, 0, 0), Corner(2, 2, 0), Corner(2, 2, 2), Corner(2, 0, 2), +}; + +struct PatchDraw { + TessellationConfiguration configuration; + std::span indices; + float minLevel = 1.0f; + float maxLevel = 64.0f; +}; + +const PatchDraw Triangles{Patches, Indices}; + struct Stages { ShaderRecompiler::RecompileResult local; ShaderRecompiler::RecompileResult hull; @@ -131,7 +232,7 @@ struct Stages { ShaderRecompiler::RecompileResult fragment; }; -ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, std::uint32_t& pushCursor) { +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)}}}; @@ -149,13 +250,15 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, Patches, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1}}; + graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1}}; } + auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; + if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; const ShaderRecompiler::RecompileRequest request{ {stage, reinterpret_cast(code.data()), code, 0, {}}, {64, hull ? 0u : 8u, userData, std::nullopt, pixel, vertex, memory}, target, - stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}, + layout, graphics }; auto result = ShaderRecompiler::Recompile(request); @@ -163,10 +266,10 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t return result; } -void ExpectRefusal(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, const std::string& reason) { +void ExpectRefusal(const ShaderRecompiler::SpirvTarget& target, ShaderStage stage, std::span code, const std::string& reason, const PatchDraw& patches = Triangles) { std::uint32_t cursor = 0; try { - static_cast(Compile(target, stage, code, cursor)); + static_cast(Compile(target, stage, code, cursor, patches)); } catch (const std::exception& error) { Require(std::string(error.what()).find(reason) != std::string::npos, "the refusal named another reason: " + std::string(error.what())); return; @@ -193,7 +296,7 @@ std::uint32_t Row(float y) { return static_cast((1.0f - y) * 0.5f * Height); } -void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front) { +void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags cull, VkFrontFace front, const PatchDraw& patches = Triangles) { for (std::size_t i = 0; i < Pixels.size(); i += 4) { for (std::size_t c = 0; c < 4; ++c) Pixels[i + c] = std::byte{Background[c]}; } @@ -210,7 +313,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags {ShaderStage::Fragment, &stages.fragment, PixelPushOffset(cursor, device.Target())}, }}; AgcDriver::Graphics::State state{}; - state.stages = {AgcDriver::Graphics::ShaderPath::Tessellation, 0x0200210du, 64, 64, std::nullopt, Patches}; + state.stages = {AgcDriver::Graphics::ShaderPath::Tessellation, 0x0200210du, 64, 64, std::nullopt, patches.configuration, patches.minLevel, patches.maxLevel}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; @@ -224,7 +327,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - device.Draw(state, {reinterpret_cast(Indices.data()), static_cast(Indices.size()), 2, 1, 0, true}, shaders); + device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1, 0, true}, shaders); device.WaitIdle(); } @@ -241,27 +344,105 @@ 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) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles) { 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), + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), }; const auto center = Row(-0.5f + 1.0f / 3.0f); for (const auto cull : {VK_CULL_MODE_NONE, VK_CULL_MODE_BACK_BIT}) { - Draw(device, stages, cull, VK_FRONT_FACE_COUNTER_CLOCKWISE); + Draw(device, stages, cull, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); const std::string name = program + (cull == VK_CULL_MODE_NONE ? ", unculled" : ", back-face culled"); Require(CoveredPixels(0, false) > 100 && CoveredPixels(0, true) == 0, name + ": the level-1 patch was not drawn as one flat triangle, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); Require(CoveredPixels(1, false) == 0, name + ": the patch with level 0 was not culled"); Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, name + ": the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); Require(PixelAt(0, 0) == Background && PixelAt(Width - 1, Height - 1) == Background, name + ": the corners changed"); } - Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, patches); Require(CoveredPixels(0, false) == 0 && CoveredPixels(2, false) == 0, program + ": tessellated triangles did not keep the control points' winding"); } +Stages CompileTriangles(const ShaderRecompiler::SpirvTarget& target, const PatchDraw& patches) { + std::uint32_t cursor = 0; + return { + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; +} + +void RequireCounterClockwise(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const PatchDraw patches{{3u, 3u, 1u, 2u, 3u}, Indices}; + const auto stages = CompileTriangles(target, patches); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) == 0 && CoveredPixels(2, false) == 0, "counter-clockwise output kept the winding of clockwise output"); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 100 && CoveredPixels(2, false) > 100, "counter-clockwise output did not reverse the control points' winding"); +} + +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); + 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))); + Require(CoveredPixels(1, false) == 0, "VGT_HOS_MAX_TESS_LEVEL 1 kept the patch with level 0"); + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, Triangles); + Require(CoveredPixels(2, false) > 100 && PixelAt(Column(2, 0.25f), center)[1] > 32, "the same hull kept the previous draw's VGT_HOS_MAX_TESS_LEVEL 1 for a draw with level 64, centre " + PixelText(PixelAt(Column(2, 0.25f), center))); + const PatchDraw raised{Patches, Indices, 5.0f, 64.0f}; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, raised); + Require(PixelAt(Column(0, 0.25f), center)[1] > 32, "VGT_HOS_MIN_TESS_LEVEL 5 did not tessellate the level-1 patch, centre " + PixelText(PixelAt(Column(0, 0.25f), center))); + Require(CoveredPixels(1, false) == 0, "VGT_HOS_MIN_TESS_LEVEL 5 raised the patch with level 0 instead of culling it"); +} + +const PatchDraw RecordPatches{{3u, 3u, 1u, 2u, 2u, 63u}, Indices}; + +void RequirePatchConstants(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, RecordPatches), + Compile(target, ShaderStage::TessellationControl, HullPatchConstant, cursor, RecordPatches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, RecordPatches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, RecordPatches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, RecordPatches); + const auto center = Row(-0.5f + 1.0f / 3.0f); + for (const auto& [patch, blue] : {std::pair{0u, 32}, std::pair{2u, 159}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - blue) <= 1, "patch " + std::to_string(patch) + " did not take its blue channel from the patch constant its hull wrote, " + PixelText(pixel)); + } + Require(PixelAt(Column(1, 0.25f), center) == Background, "the patch with level 0 was not culled"); +} + +void RequireQuads(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(0.0f); + std::array frontFacing{}; + for (const std::uint32_t topology : {2u, 3u}) { + const PatchDraw patches{{4u, 4u, 2u, 2u, topology}, QuadIndices, 1.0f, 9.0f}; + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, QuadHullCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, QuadDomainCode, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + const std::string name = topology == 2u ? "clockwise quad patches" : "counter-clockwise quad patches"; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + Require(CoveredPixels(0, false) > 2000 && CoveredPixels(0, true) == 0, name + ": the level-1 patch was not drawn as one flat quad, " + std::to_string(CoveredPixels(0, false)) + " pixels"); + Require(PixelAt(Column(0, 0.02f), Row(0.45f)) != Background && PixelAt(Column(0, 0.48f), Row(0.45f)) != Background, name + ": the level-1 patch misses its upper corners"); + Require(CoveredPixels(1, false) == 0, name + ": the patch with level 0 was not culled"); + Require(CoveredPixels(2, false) > 2000 && PixelAt(Column(2, 0.25f), center)[1] > 32, name + ": the level-5 patch has no interior domain points at its centre, " + PixelText(PixelAt(Column(2, 0.25f), center))); + Draw(device, stages, VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + frontFacing[topology - 2u] = CoveredPixels(0, false) > 2000; + Require(frontFacing[topology - 2u] == (CoveredPixels(2, false) > 2000) && (frontFacing[topology - 2u] || CoveredPixels(0, false) == 0), name + ": back-face culling kept part of a patch"); + } + Require(frontFacing[0] != frontFacing[1], "clockwise and counter-clockwise quad patches have the same winding"); +} + struct RegisteredStage { Shader shader{}; std::array registers{}; @@ -321,14 +502,27 @@ int main() { return VulkanTestSkipped; } ExpectRefusal(target, ShaderStage::TessellationControl, HullUnguardedFactors, "not written by control point 0 alone"); - ExpectRefusal(target, ShaderStage::TessellationControl, HullSquaredAddress, "not affine"); + 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::TessellationEvaluation, DomainPatchRead, "patch constants"); + 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, 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}); + ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "outside its control points and patch constants", {{3u, 3u, 1u, 2u, 2u, 129u}, Indices}); RequirePatches(*device, target, HullCode, "hull program"); RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); + RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + 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); + RequireQuads(*device, target); + RequirePatchConstants(*device, 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 f03fe5dbbe..8576fd86e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Uint8x4ColorTarget.cpp @@ -133,17 +133,18 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info, AgcDriver::Graphics::ColorTileMode mode, std::uint32_t pipeBankXor) { AgcDriver::Registers cx; - cx[0x318] = static_cast(block.Address() >> 8u); + const auto address = block.Address() + pipeBankXor; + cx[0x318] = static_cast(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); + cx[0x3b8] = LinearAttrib3 | (static_cast(mode) << 14u); + cx[0x390] = static_cast(address >> 40u); auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); color.exportIndex = 0; color.uintExport = true; @@ -218,15 +219,17 @@ std::vector ReadBack(AgcDriver::VulkanDevice& device, std::uint64_ return stored; } -void Expect(AgcDriver::VulkanDevice& device, std::uint32_t info, VkFormat format, std::uint32_t elementBytes, std::array exported, const std::string& what) { +void Expect(AgcDriver::VulkanDevice& device, std::uint32_t info, VkFormat format, std::uint32_t elementBytes, std::array exported, const std::string& what, AgcDriver::Graphics::ColorTileMode mode = AgcDriver::Graphics::ColorTileMode::Linear, std::uint32_t pipeBankXor = 0) { const auto bytes = static_cast(Width) * Height * elementBytes; - Require(AgcDriver::Graphics::ColorTargetLayout(Width, Height, AgcDriver::Graphics::ColorTileMode::Linear, elementBytes).Bytes() == bytes, what + ": the linear target is padded"); - Block block(bytes); - std::memset(block.data, Kept, bytes); - const auto color = DecodeTarget(block, info); - Require(color.format == format && color.elementBytes == elementBytes && color.bytes == bytes, what + ": the target decoded to the wrong format"); + const AgcDriver::Graphics::ColorTargetLayout layout(Width, Height, mode, elementBytes, pipeBankXor); + Block block(layout.Bytes()); + std::memset(block.data, Kept, block.bytes); + const auto color = DecodeTarget(block, info, mode, pipeBankXor); + Require(color.format == format && color.elementBytes == elementBytes && color.bytes == layout.Bytes() && color.tileMode == mode && color.pipeBankXor == pipeBankXor, what + ": the target decoded to the wrong format, mode or XOR"); Draw(device, color, Program(exported)); - const auto stored = ReadBack(device, block.Address(), bytes); + const auto tiled = ReadBack(device, block.Address(), block.bytes); + std::vector stored(bytes); + layout.Detile(std::as_bytes(std::span(tiled)), std::as_writable_bytes(std::span(stored))); for (std::size_t offset = 0; offset < bytes; ++offset) { const bool inside = (offset / elementBytes) % Width < ScissorWidth; const auto expected = inside ? static_cast(exported[offset % elementBytes]) : Kept; @@ -241,6 +244,8 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; 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); 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/VectorWindowDirectAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowDirectAccess.cpp new file mode 100644 index 0000000000..0f02fa9bd9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowDirectAccess.cpp @@ -0,0 +1,125 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 14; +constexpr std::uint32_t WrittenLane = 3; +constexpr std::uint32_t ReadLane = 5; +constexpr std::uint32_t LaneValue = 0x5a5a0000u; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0xe0301004, 0x80000b01, 0xbe8803ff, 0x5a5a0000, + 0xbf8c3f70, 0x4a100881, 0x4a120882, 0x7e140304, 0xd761000a, 0x00010608, 0xd7600009, 0x0001070a, + 0xd760000a, 0x00010b0a, 0x4a180809, 0xbefc0380, 0x7e208708, 0xbefc0381, 0x7e228708, 0xbefc0382, + 0x7e248708, 0xbefc0383, 0x7e268708, 0xbefc0384, 0x7e288708, 0x34120884, 0xbefc0381, 0x7e2a8708, + 0xbefc0388, 0x7e2c8708, 0x7e2e0209, 0x7e30020a, 0xe0701000, 0x80010803, 0xe0701004, 0x80010903, + 0xe0701008, 0x80010a03, 0xe070100c, 0x80010b03, 0xe0701010, 0x80010c03, 0xe0701014, 0x80011003, + 0xe0701018, 0x80011103, 0xe070101c, 0x80011203, 0xe0701020, 0x80011303, 0xe0701024, 0x80011403, + 0xe0701028, 0x80011503, 0xe070102c, 0x80011603, 0xe0701030, 0x80011703, 0xe0701034, 0x80011803, 0xbf810000, +}; + +constexpr const char* Names[Checked] = {"valu_direct", "rewritten_direct", "writelane_direct", "loaded_direct", "readlane_sum_direct", "movrels_valu", "movrels_valu_neighbour", + "movrels_writelane", "movrels_loaded", "movrels_readlane_sum", "movrels_rewritten", "movrels_of_movrels_result", "readlane_written_lane", "readlane_other_lane"}; + +std::uint32_t First(std::uint32_t tid) { + return 0x1000u * (tid + 1u); +} + +std::uint32_t Second(std::uint32_t tid) { + return 0x00770000u + tid * 0x101u; +} + +std::array ExpectedFor(std::uint32_t tid) { + const std::uint32_t x = First(tid); + const std::uint32_t y = Second(tid); + const std::uint32_t lane = tid == WrittenLane ? LaneValue : x; + return {x + 1u, x << 4u, lane, y, x + LaneValue, x + 1u, x + 2u, lane, y, x + LaneValue, x << 4u, x + 1u, LaneValue, First(ReadLane)}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < threads; ++tid) { + Input[tid * Inputs] = First(tid); + Input[tid * Inputs + 1u] = Second(tid); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const auto expected = ExpectedFor(tid); + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(out[index] == expected[index], std::string("vector window direct access: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(out[index]) + ", expected " + Hex(expected[index])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); + std::puts("vector window direct access tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp new file mode 100644 index 0000000000..fe7d3b448f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -0,0 +1,176 @@ +#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 Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xe0382010, 0x80000405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y, XAndY }; + +struct Row { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Rows{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, + {"clip planes 0x5, components 0 and 2", DistanceVector | 0x5u, Visible::XAndY, PositionX, Negative, PositionY}, + {"cull plane 0, every vertex negative", DistanceVector | 0x100u, Visible::None, Negative, Negative, Negative}, + {"cull plane 0, one vertex not negative", DistanceVector | 0x100u, Visible::All, Mixed, Negative, Negative}, + {"cull plane 0 from component 2, sparse 0x4", DistanceVector | 0x400u, Visible::None, Mixed, Negative, Negative}, +}}; + +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}; +} + +bool Expected(Visible visible, std::uint32_t pixel) { + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + switch (visible) { + case Visible::All: return true; + case Visible::None: return false; + case Visible::X: return x >= 0.0; + case Visible::Y: return y >= 0.0; + case Visible::XAndY: return x >= 0.0 && y >= 0.0; + } + return false; +} + +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row) { + 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); + 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))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.paClVsOutCntl = row.control; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory}, + device.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.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + 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, std::vector(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory}, + device.Target(), + PixelPushLayout(vertexPush, device.Target()) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &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}; + state.paClVsOutCntl = row.control; + 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(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); + 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)); + const bool written = value == 255u; + const bool expected = Expected(row.visible, pixel); + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const auto waveSize : {64u, 32u}) { + for (const auto& row : Rows) { + Draw(*device, waveSize, row); + Check(row, waveSize); + } + } + std::puts("vertex clip and cull distance 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/VertexClipCullDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp new file mode 100644 index 0000000000..84b788a199 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDraw.cpp @@ -0,0 +1,244 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t DistanceVector = 0x00400000u; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; + +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70, + 0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +struct Vertex { + std::array position; + std::array distances; +}; + +constexpr std::array PositionX{-3.0f, 1.0f, 1.0f}; +constexpr std::array PositionY{-1.0f, -1.0f, 3.0f}; +constexpr std::array Negative{-1.0f, -1.0f, -1.0f}; +constexpr std::array Mixed{-1.0f, 1.0f, -1.0f}; + +enum class Visible { All, None, X, Y }; + +struct Step { + const char* name; + std::uint32_t control; + Visible visible; + std::array component0; + std::array component1; + std::array component2; +}; + +const std::array Steps{{ + {"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative}, + {"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative}, + {"cull plane 0 from component 2, not every vertex negative", DistanceVector | 0x400u, Visible::All, Negative, Negative, Mixed}, + {"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step, const std::array& vertices) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x207, step.control); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast(vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("vertex clip and cull distances through the driver, ") + step.name; + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto value = std::to_integer(colors[pixel * 4u]); + Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value)); + const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width; + const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height; + const bool expected = step.visible == Visible::All || (step.visible == Visible::X && x >= 0.0) || (step.visible == Visible::Y && y >= 0.0); + const bool written = value == 255u; + Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear")); + } +} + + +// A program that exports only pos0 is compiled without the PA_CL_VS_OUT_CNTL position export bits, at +// registration and at draw alike; one that exports pos1 keeps them, without the bits outside the layout. +void PositionExportLayoutTests() { + using AgcDriver::DriverDetail::PositionExportLayout; + constexpr std::array PositionOnly{0xf80008cfu, 0x03020100u, 0xbf810000u}; + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0040000fu) == 0x0040000fu, "a program exporting pos1 lost its position export bits"); + Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0144000fu) == 0x0040000fu, "bits outside the position export layout reached the vertex info"); + Require(PositionExportLayout(nullptr, 0, PositionOnly, 0x0040000fu) == 0u, "a program exporting only pos0 kept the position export bits"); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{0x10000u, 0u, 1u, {PositionOnly.begin(), PositionOnly.end()}, {}}; + for (int use = 0; use < 2; ++use) Require(PositionExportLayout(&snapshot, 0, snapshot.code, 0x0040000fu) == 0u, "a cached pos0-only program kept the position export bits"); + snapshot.code.assign(VertexCode.begin(), VertexCode.end()); + Require(PositionExportLayout(&snapshot, 2, std::span(snapshot.code).subspan(2), 0x0040000fu) == 0x0040000fu, "a program at an offset was answered from the cache of its snapshot"); +} + +} + +int main(int argc, char** argv) { + try { + const bool noKey = argc > 1 && std::string(argv[1]) == "no-key"; + if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set"); + PositionExportLayoutTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(2 * Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (const auto& step : Steps) { + std::array vertices{}; + for (std::size_t i = 0; i < vertices.size(); ++i) { + vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}}; + } + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step, vertices); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + AgcDriverShutdown_nid_postfix(); + std::puts(noKey ? "vertex clip and cull distance draws passed without the draw key" : "vertex clip and cull distance draws 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/Wave32ExecLaneBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Wave32ExecLaneBits.cpp new file mode 100644 index 0000000000..a785e4ef2c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Wave32ExecLaneBits.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x340c0082u, + 0x7d88008du, + 0x02020080u, + 0xbe8a3c6au, + 0x4a0202c0u, + 0x887e0a7eu, + 0x7d820094u, + 0xbe8b3c6au, + 0x4a0202a0u, + 0xbefe030bu, + 0x7d82008au, + 0x7e0e0281u, + 0x02040e80u, + 0x4a020302u, + 0xe0701000u, 0x80000106u, + 0xbf810000u, +}; + +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::uint32_t Expected(std::uint32_t lane) { + std::uint32_t value = lane < 13u ? lane + 64u : 0u; + if (lane > 20u) value += 32u; + if (lane > 10u) value += 1u; + return value; +} + +std::size_t Ballots(std::span words) { + std::size_t count = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpGroupNonUniformBallot) ++count; + } + return count; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Output.fill(0xdeadbeefu); + std::vector userData(4, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + const auto ballots = Ballots(result.spirv.Words()); + Require(ballots == 0u, "wave32 compares, saveexec and exec restores emitted " + std::to_string(ballots) + " subgroup ballots, expected none"); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Expected(lane), "wave32 lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(Expected(lane))); + } + std::puts("wave32 exec lane bits 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/WriteDataWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/WriteDataWrittenAhead.cpp new file mode 100644 index 0000000000..529aad4163 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/WriteDataWrittenAhead.cpp @@ -0,0 +1,105 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +alignas(256) std::array First{}; +alignas(256) std::array Stale{}; +alignas(256) std::array Fresh{}; +alignas(256) std::array FirstDescriptor{}; +alignas(256) std::array SecondDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Count(std::vector& words, const void* descriptor) { + Program(words, CounterCode.data(), {Low(descriptor), High(descriptor)}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + FirstDescriptor = Descriptor(First.data(), sizeof(First)); + SecondDescriptor = Descriptor(Stale.data(), sizeof(Stale)); + std::vector warmUp; + Count(warmUp, FirstDescriptor.data()); + Count(warmUp, SecondDescriptor.data()); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && First[0] == 1 && Stale[0] == 1, "the warm-up dispatches did not run"); + First[0] = 0; + Stale[0] = 0; + const auto fresh = Descriptor(Fresh.data(), sizeof(Fresh)); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Count(words, FirstDescriptor.data()); + words.insert(words.end(), {0xc0063700u, 0x00100200u, Low(SecondDescriptor.data()), High(SecondDescriptor.data()), fresh[0], fresh[1], fresh[2], fresh[3]}); + Count(words, SecondDescriptor.data()); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(SecondDescriptor[0] == fresh[0] && SecondDescriptor[1] == fresh[1], "WRITE_DATA did not replace the descriptor"); + Require(First[0] == 1, "the first dispatch did not run"); + Require(Fresh[0] == 1 && Stale[0] == 0, "a dispatch whose descriptor a WRITE_DATA before it replaces used the old descriptor (stale " + std::to_string(Stale[0]) + ", fresh " + std::to_string(Fresh[0]) + ")"); + std::cout << "write data written ahead 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/ZeroDwordFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroDwordFill.cpp new file mode 100644 index 0000000000..e291be51a4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroDwordFill.cpp @@ -0,0 +1,133 @@ +#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 + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::MatchZeroDwordFill; + +alignas(256) constexpr std::array Code{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +alignas(256) std::array Output{}; + +std::vector UserData(void* output, std::uint32_t records, std::uint32_t stride = 4, std::uint32_t format = 0x00014204u) { + const auto address = reinterpret_cast(output); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, format}; +} + +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 = MatchZeroDwordFill(packet, Code, data, compute); + Require(fill && fill->base == reinterpret_cast(Output.data()) && fill->bytes == 1024 && fill->pattern == std::array{}, "the zero dword fill kernel was not recognized as a zero fill"); + for (std::size_t word = 0; word < Code.size(); ++word) { + auto altered = Code; + altered[word] ^= 1u; + Require(!MatchZeroDwordFill(packet, altered, data, compute), "a different shader matched the zero dword fill kernel"); + } + Require(!MatchZeroDwordFill(packet, std::span(Code).first(5), data, compute), "a truncated shader matched"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 8), compute), "a strided descriptor matched a contiguous fill"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00814204u), compute), "an add-tid descriptor matched"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x30014204u), compute), "a raw out-of-bounds mode matched"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00011204u), compute), "an 8-bit output format matched"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00017204u), compute), "an output format above 32_FLOAT matched"); + Require(MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00015204u), compute).has_value(), "32_SINT output did not match"); + Require(MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00016204u), compute).has_value(), "32_FLOAT output did not match"); + Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched"); + auto extra = data; + extra.push_back(0); + Require(!MatchZeroDwordFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)"); + auto partial = packet; + partial[4] |= 0x20u; + Require(!MatchZeroDwordFill(partial, Code, data, compute), "a partial dispatch matched"); + auto noGroup = compute; + noGroup.groupIdEnable[0] = false; + Require(!MatchZeroDwordFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); + const auto clamped = MatchZeroDwordFill(packet, Code, UserData(Output.data(), 64), compute); + Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records"); +} + +std::vector Commands(const std::vector& data, std::uint32_t groups, bool partial, std::uint32_t threads) { + 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, partial ? threads : groups, 1, 1, 0x41u | (partial ? 0x20u : 0u)}); + return words; +} + +void ExecuteTests() { + struct Case { + std::uint32_t groups, records, stride; + bool partial; + std::uint32_t threads; + }; + constexpr std::array cases{{ + {4, 1024, 4, false, 0}, + {16, 1024, 4, false, 0}, + {4, 64, 4, false, 0}, + {4, 100, 4, false, 0}, + {4, 102, 4, false, 0}, + {2, 1024, 8, false, 0}, + {0, 1024, 4, true, 70}, + {0, 1024, 4, false, 0}, + }}; + for (const auto& item : cases) { + Output.fill(Untouched); + const auto words = Commands(UserData(Output.data(), item.records, item.stride), item.groups, item.partial, item.threads); + 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)); + const auto launched = item.partial ? item.threads : item.groups * 64u; + const auto filled = std::min(launched, item.records); + const auto step = item.stride / 4u; + for (std::uint32_t i = 0; i < Output.size(); ++i) { + const bool written = i % step == 0 && i / step < filled; + const auto expected = written ? 0u : Untouched; + Require(Output[i] == expected, "zero dword fill mismatch at " + std::to_string(i) + " (groups " + std::to_string(item.groups) + ", records " + std::to_string(item.records) + ", stride " + std::to_string(item.stride) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests(); + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("zero dword 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/shaders/BufferUpdate.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/BufferUpdate.comp new file mode 100644 index 0000000000..b9332d0804 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/BufferUpdate.comp @@ -0,0 +1,5 @@ +#version 450 +layout(local_size_x = 1) in; +layout(set = 0, binding = 0, std430) buffer Values { uint values[]; }; +layout(push_constant) uniform Update { uint delta; } update; +void main() { values[gl_GlobalInvocationID.x] += update.delta; } diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRange.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRange.vert new file mode 100644 index 0000000000..f19a9cc564 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRange.vert @@ -0,0 +1,7 @@ +#version 450 +layout(location = 0) in vec4 offset; +out gl_PerVertex { vec4 gl_Position; }; +void main() { + vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(positions[gl_VertexIndex % 3] + vec2(offset.x, 0.0), 0.5, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag new file mode 100644 index 0000000000..c7225d0c00 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/VertexRangeProducer.frag @@ -0,0 +1,5 @@ +#version 450 +layout(location = 0) out vec4 color; +void main() { + color = vec4(0.5, 0.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/trace_revision.cmake b/core/libs/prx/libSceAgcDriver/trace_revision.cmake new file mode 100644 index 0000000000..a63974e68e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/trace_revision.cmake @@ -0,0 +1,19 @@ +set(commit unknown) +set(dirty -1) +if(GIT) + execute_process(COMMAND "${GIT}" -C "${SOURCE}" rev-parse --short=12 HEAD + OUTPUT_VARIABLE head RESULT_VARIABLE failed OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(failed EQUAL 0 AND NOT head STREQUAL "") + set(commit ${head}) + execute_process(COMMAND "${GIT}" -C "${SOURCE}" status --porcelain --untracked-files=no + OUTPUT_VARIABLE changes RESULT_VARIABLE failed ERROR_QUIET) + if(failed EQUAL 0 AND changes STREQUAL "") + set(dirty 0) + elseif(failed EQUAL 0) + set(dirty 1) + endif() + endif() +endif() +file(WRITE "${OUTPUT}.new" "#pragma once\n#define APS5_TRACE_COMMIT \"${commit}\"\n#define APS5_TRACE_DIRTY ${dirty}\n") +execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OUTPUT}.new" "${OUTPUT}") +file(REMOVE "${OUTPUT}.new") diff --git a/core/libs/prx/libSceAmpr/CMakeLists.txt b/core/libs/prx/libSceAmpr/CMakeLists.txt index 896ffaf200..f9898075a1 100644 --- a/core/libs/prx/libSceAmpr/CMakeLists.txt +++ b/core/libs/prx/libSceAmpr/CMakeLists.txt @@ -33,4 +33,20 @@ if(BUILD_TESTING) set_tests_properties(guest_ampr_threads PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + enable_language(ASM) + add_test_executable(guest_ampr_cpp_error_tests + ${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.S) + target_include_directories(guest_ampr_cpp_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ampr_cpp_error_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_ampr_cpp_error_tests) + add_test(NAME guest_ampr_cpp_error COMMAND guest_ampr_cpp_error_tests) + set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_ampr_cpp_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() endif() diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index db2eb580c0..fba6d9efbe 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; @@ -20,8 +21,13 @@ static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static std::uint32_t RemainingCommandBytes(const Apr::CommandBufferObject& buffer) { + if (buffer.offset > buffer.size) throw std::invalid_argument("AMPR: command buffer offset exceeds bound memory"); + return buffer.size - buffer.offset; +} + static int Append(Apr::CommandBufferObject* buffer, const void* command, uint32_t bytes) { - if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; std::memcpy(buffer->base + buffer->offset, command, bytes); buffer->offset += bytes; ++buffer->numCommands; @@ -87,7 +93,7 @@ static std::uint64_t NopBytes(std::uint32_t dwords) { static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); @@ -110,7 +116,7 @@ static std::uint64_t MeasureMarker(const char* text) { static int AppendMarker(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, const char* text) { if (!buffer || !text) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = MarkerBytes(text); - if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; const Apr::MarkerCommand command{{opcode, static_cast(bytes)}}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); @@ -137,7 +143,7 @@ static bool ValidMap(std::uint64_t address, std::uint64_t directOffset, std::uin static constexpr std::int64_t AmmMeasureInvalid = SCE_KERNEL_ERROR_EINVAL; static int AppendAmm(Apr::CommandBufferObject* buffer, const void* command, std::uint32_t bytes) { - if (bytes > buffer->size - buffer->offset) return SCE_KERNEL_ERROR_EBUSY; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; return Append(buffer, command, bytes); } @@ -181,7 +187,7 @@ static constexpr std::int32_t PrtAllocationMask = 1019; static int RecordPrt(Apr::CommandBufferObject* buffer, const void* command, std::uint32_t bytes) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; - if (bytes > buffer->size - buffer->offset) return SCE_KERNEL_ERROR_EBUSY; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; return Append(buffer, command, bytes); } @@ -219,15 +225,76 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v } static constexpr int ThrdSuccess = 0; +static constexpr int ThrdNomem = 1; +static constexpr int ThrdTimedout = 2; +static constexpr int ThrdBusy = 3; static constexpr int ThrdError = 4; +static constexpr int CppDeviceOrResourceBusy = 0; +static constexpr int CppInvalidArgument = 1; +static constexpr int CppResourceUnavailableTryAgain = 6; + +static constexpr int MtxPlain = 0x1; + static std::atomic_flag sharedPtrSpinLock; +struct AmmUsageStatsData { + std::uint64_t sizeInBytes; + std::uint16_t numPageTablePoolEntries; + std::uint16_t allocatedPageTablePoolEntries; + std::uint16_t highWatermarkAllocatedEntries; + std::uint16_t reserved; + std::uint32_t ringIdleFlags; +}; +static_assert(sizeof(AmmUsageStatsData) == 0x18); +static constexpr std::uint16_t AmmPageTablePoolEntries = 512; +static constexpr std::uint32_t AmmRingIdleFlags = 0x7; + +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + extern "C" { +int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); +[[noreturn]] void ThrowCppError_nid_no_patch(int code); +} + +static void RequireThrd(int result, const char* what) { + switch (result) { + case ThrdSuccess: + return; + case ThrdNomem: + case ThrdTimedout: + ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); + case ThrdBusy: + ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); + case ThrdError: + ThrowCppError_nid_no_patch(CppInvalidArgument); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); } extern "C" { @@ -831,16 +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 _ZSt16_Throw_Cpp_errori_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { void* result = nullptr; if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; @@ -848,19 +908,36 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { return ThrdSuccess; } -int APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); } -int APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); + if (created != ThrdSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != ThrdSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } } -int APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); } int APS5_VABI _Thrd_id_nid_postfix(void) { @@ -868,20 +945,17 @@ int APS5_VABI _Thrd_id_nid_postfix(void) { return 0; } -int APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); } - int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -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); } @@ -890,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; @@ -910,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; @@ -995,8 +1059,13 @@ int APS5_VABI sceAmprFpclassifyd(double value) { return SonyFpclassifyd(value); } -int APS5_VABI sceAmprAmmGetUsageStatsData() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAmprAmmGetUsageStatsData(AmmUsageStatsData* stats) { + if (!stats || stats->sizeInBytes > sizeof(AmmUsageStatsData)) return SCE_KERNEL_ERROR_EINVAL; + AmmUsageStatsData out{}; + out.sizeInBytes = stats->sizeInBytes; + out.numPageTablePoolEntries = AmmPageTablePoolEntries; + out.ringIdleFlags = AmmRingIdleFlags; + std::memcpy(stats, &out, static_cast(stats->sizeInBytes)); return 0; } } diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index ed74e8f724..5f56318a14 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -93,6 +93,15 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } } +int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) { + if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); + if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory); + ClearDirectory(directory); + return 0; +} + int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; @@ -163,9 +172,10 @@ int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint* moun } -int APS5_VABI sceAppContentAddcontEnqueueDownload(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentAddcontEnqueueDownload(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) { diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index bc11e8e5b1..e499cbf081 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -67,6 +67,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_ex_monitor_info_tests tests/AudioOutExMonitorInfo.cpp) + target_include_directories(audio_out_ex_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_ex_monitor_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_ex_monitor_info_tests) + add_test(NAME audio_out_ex_monitor_info COMMAND audio_out_ex_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_ex_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_latency_tests tests/AudioOut2Latency.cpp) target_include_directories(audio_out2_latency_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_latency_tests PRIVATE libSceAudioOut libc) @@ -101,7 +111,7 @@ if(BUILD_TESTING) configure_windows_unwind(audio_out2_timing_tests) add_test(NAME audio_out2_timing COMMAND audio_out2_timing_tests) if(WIN32) - set_tests_properties(audio_out2_timing PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(audio_out2_timing PROPERTIES RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index c8adb77b2a..923d459f2f 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -35,6 +35,7 @@ static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; static constexpr int MONITOR_TYPE_HDMI = 1; static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; +static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; enum class Format { Unknown, @@ -499,6 +500,11 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } +int APS5_VABI sceAudioOutDeviceIdOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceAudioOutSysClose() { NotImplemented_nid_no_patch(__func__); return 0; @@ -557,8 +563,17 @@ int APS5_VABI sceAudioOutExConfigureOutput() { return 0; } -int APS5_VABI sceAudioOutExGetMonitorInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std::uint32_t size) { + if (type != 0 || index != 0) { + throw std::runtime_error("sceAudioOutExGetMonitorInfo: monitor " + std::to_string(type) + ", " + std::to_string(index) + " not supported"); + } + if (size < EX_MONITOR_INFO_MIN_SIZE) { + throw std::runtime_error("sceAudioOutExGetMonitorInfo: size " + std::to_string(size) + " not supported"); + } + if (info == nullptr) { + return -2144993276; + } + std::memset(info, 0, size); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index dff7965280..6ac66b3741 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -1,11 +1,15 @@ +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "AudioOut2Internal.hpp" static constexpr int USER_ID_SYSTEM = 0xFF; static constexpr std::uint32_t MIN_SUPPORTED_3D_LATENCY = 1; static constexpr std::uint32_t MAX_SUPPORTED_3D_LATENCY = 2; +static std::atomic g_chatEnabled{false}; + extern "C" { int APS5_VABI sceAudioOut2Initialize(void) { @@ -29,13 +33,8 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar return 0; } -int APS5_VABI sceAudioOut2UserGetSupportedAttributes(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioOut2EnableChat(void) { - NotImplemented_nid_no_patch(__func__); + if (!g_chatEnabled.exchange(true)) AUDIOOUT2_TRACE("t=%.3f EnableChat: chat audio is allowed alongside the title's; the host mixes no chat audio\n", AudioOut2TraceSeconds()); return 0; } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp index a139d22626..a8c42717bf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp @@ -1,23 +1,44 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include +#include +#include +#include +#include static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast(0x80260502); -static std::atomic g_nextUser{1}; +static std::mutex g_usersLock; +static AudioOut2UserHandle g_nextUser = 1; +static std::unordered_set g_users; extern "C" { int APS5_VABI sceAudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle) { (void)user_id; if (!handle) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; - *handle = g_nextUser.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_usersLock); + if (g_nextUser == std::numeric_limits::max()) + throw std::runtime_error("AudioOut2: user handles exhausted"); + g_users.insert(g_nextUser); + *handle = g_nextUser++; return 0; } int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) { - (void)handle; + std::lock_guard lock(g_usersLock); + g_users.erase(handle); + return 0; +} + +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle, uint32_t* contextAttributes, uint32_t* portAttributes) { + if (!contextAttributes || !portAttributes) + throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: null output"); + std::lock_guard lock(g_usersLock); + if (!g_users.contains(handle)) + throw std::runtime_error("sceAudioOut2UserGetSupportedAttributes: unknown user handle"); + *contextAttributes = 0; + *portAttributes = (1u << 0) | (1u << 1); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 5f6eea7b10..7482925465 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceAudioOut2Set3DLatency(int, std::uint32_t); 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(); } static void Require(bool value, const char* message) { @@ -60,6 +61,11 @@ void TestMasteringSetParam() { Require(ThrowsRuntimeError([] { sceAudioOut2MasteringSetParam(nullptr, 0, 0); }), "null mastering parameters must throw"); } +void TestEnableChat() { + Require(sceAudioOut2EnableChat() == 0, "enabling chat must be accepted"); + Require(sceAudioOut2EnableChat() == 0, "enabling chat again must be accepted"); +} + } int main() { @@ -68,5 +74,6 @@ int main() { TestMasteringInit(); TestMasteringTerm(); TestMasteringSetParam(); + TestEnableChat(); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp index bf6c33e176..1cad3a7422 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortLayouts.cpp @@ -9,10 +9,15 @@ #include #include #include +#include #include +#include #include extern "C" { +int APS5_VABI sceAudioOut2UserCreate(std::uint32_t, AudioOut2UserHandle*); +int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle); +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(AudioOut2UserHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); @@ -48,6 +53,51 @@ constexpr float masterGain = 0.5f; constexpr float fold = 0.70710678f; constexpr float tolerance = 1e-5f; +void TestUserAttributes() { + struct Output { + std::uint32_t before = 0x12345678u; + std::uint32_t value = 0xFFFFFFFFu; + std::uint32_t after = 0x87654321u; + } context, port; + AudioOut2UserHandle first = 0; + AudioOut2UserHandle second = 0; + Require(sceAudioOut2UserCreate(1, &first) == 0); + Require(sceAudioOut2UserCreate(2, &second) == 0 && second != first); + Require(sceAudioOut2UserGetSupportedAttributes(first, &context.value, &port.value) == 0); + Require(context.value == 0 && port.value == ((1u << attributeData) | (1u << attributeVolume))); + Require(context.before == 0x12345678u && context.after == 0x87654321u); + Require(port.before == 0x12345678u && port.after == 0x87654321u); + const auto rejects = [&](AudioOut2UserHandle handle, bool nullContext, bool nullPort) { + context.value = port.value = 0xFFFFFFFFu; + bool threw = false; + try { + sceAudioOut2UserGetSupportedAttributes(handle, nullContext ? nullptr : &context.value, nullPort ? nullptr : &port.value); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("sceAudioOut2UserGetSupportedAttributes") != std::string::npos; + } + Require(threw && context.value == 0xFFFFFFFFu && port.value == 0xFFFFFFFFu); + }; + rejects(first, true, false); + rejects(first, false, true); + rejects(first, true, true); + rejects(0, false, false); + rejects(std::numeric_limits::max(), false, false); + Require(sceAudioOut2UserDestroy(first) == 0); + rejects(first, false, false); + std::thread([&] { + Require(sceAudioOut2UserGetSupportedAttributes(second, &context.value, &port.value) == 0); + }).join(); + Require(context.value == 0 && port.value == 3); + AudioOut2UserHandle next = 0; + Require(sceAudioOut2UserCreate(1, &next) == 0 && next != first && next != second); + rejects(first, false, false); + Require(sceAudioOut2UserGetSupportedAttributes(next, &context.value, &port.value) == 0); + Require(context.value == 0 && port.value == 3); + Require(sceAudioOut2UserDestroy(next) == 0 && sceAudioOut2UserDestroy(second) == 0); + Require(context.before == 0x12345678u && context.after == 0x87654321u); + Require(port.before == 0x12345678u && port.after == 0x87654321u); +} + struct Layout { std::uint32_t channels; std::vector left; @@ -282,6 +332,7 @@ void TestMeasuredMainPortFormats() { } int main() { + TestUserAttributes(); SetEnvironment("SDL_AUDIODRIVER", "disk"); for (const Layout* layout : {&mono, &stereo, &surround71, &surround714}) { TestFloatLayout(*layout); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp index 8cc1266ea6..22f70c9128 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -7,7 +7,6 @@ extern "C" { int APS5_VABI sceAudioOutExClose(); int APS5_VABI sceAudioOutExConfigureOutput(); -int APS5_VABI sceAudioOutExGetMonitorInfo(); int APS5_VABI sceAudioOutExOpen(); } @@ -28,7 +27,6 @@ static bool ReportsNotImplemented(ExFunction function, const char* name) { int main() { bool passed = ReportsNotImplemented(sceAudioOutExClose, "sceAudioOutExClose"); passed = ReportsNotImplemented(sceAudioOutExConfigureOutput, "sceAudioOutExConfigureOutput") && passed; - passed = ReportsNotImplemented(sceAudioOutExGetMonitorInfo, "sceAudioOutExGetMonitorInfo") && passed; passed = ReportsNotImplemented(sceAudioOutExOpen, "sceAudioOutExOpen") && passed; return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp new file mode 100644 index 0000000000..fecdf065e1 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutExMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutExGetMonitorInfo(int, int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr std::uint32_t titleSize = 0x400; +constexpr std::uint32_t minimumSize = 0xA0; +constexpr std::size_t descriptorCountOffset = 0x98; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, int index, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutExGetMonitorInfo(type, index, info.data(), size); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == expected) return true; + std::fprintf(stderr, "threw: %s\n", error.what()); + return false; + } + return false; +} + +void CheckEmpty(std::uint32_t size) { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutExGetMonitorInfo(0, 0, buffer.data(), size) == 0, "the monitor query must succeed"); + for (std::uint32_t i = 0; i < size; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = size; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past the declared size may be written"); + } +} + +} + +int main() { + CheckEmpty(titleSize); + CheckEmpty(minimumSize); + Require(sceAudioOutExGetMonitorInfo(0, 0, nullptr, titleSize) == invalidPointer, + "a null buffer must report an invalid pointer"); + Require(Throws(0, 0, minimumSize - 1, "sceAudioOutExGetMonitorInfo: size 159 not supported"), + "a buffer without room for the descriptor count must throw"); + Require(Throws(1, 0, titleSize, "sceAudioOutExGetMonitorInfo: monitor 1, 0 not supported"), + "a nonzero first argument must throw"); + Require(Throws(0, 1, titleSize, "sceAudioOutExGetMonitorInfo: monitor 0, 1 not supported"), + "a nonzero second argument must throw"); + return 0; +} 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/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 59e62f64b1..1ea0a503ae 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -15,7 +15,7 @@ if(BUILD_TESTING) add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentSearch libSceContentDelete libc) + target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentDelete libc) configure_windows_unwind(guest_content_unimplemented_tests) add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) if(WIN32) diff --git a/core/libs/prx/libSceContentSearch/CMakeLists.txt b/core/libs/prx/libSceContentSearch/CMakeLists.txt new file mode 100644 index 0000000000..0c46ea1108 --- /dev/null +++ b/core/libs/prx/libSceContentSearch/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceContentSearch SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceContentSearch PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceContentSearch) + +if(BUILD_TESTING) + add_test_executable(guest_content_search_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentSearch.cpp) + target_include_directories(guest_content_search_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_content_search_tests PRIVATE libSceContentSearch libc) + configure_windows_unwind(guest_content_search_tests) + add_test(NAME guest_content_search COMMAND guest_content_search_tests) + if(WIN32) + set_tests_properties(guest_content_search PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index 7a40aabadf..9475f46b16 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -8,6 +8,56 @@ namespace { std::atomic g_initialized{false}; + +constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); +constexpr std::uint32_t MaxSearchLimit = 92; +constexpr std::int64_t LibraryUpdateId = 0; + +bool IsContentColumn(std::int32_t column) { + switch (column) { + case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 9: case 10: case 11: case 12: case 14: return true; + default: return false; + } +} + +bool IsSortColumn(std::int32_t column) { + switch (column) { + case 1: case 4: case 5: case 6: case 9: case 10: return true; + default: return false; + } +} + +bool IsConnector(std::int32_t connector) { return connector >= 0 && connector <= 2; } + +void ValidateColumnSets(const char* funcName, const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength) { + if (columnSet == nullptr && columnSetLength != 0) throw std::invalid_argument(std::string(funcName) + ": null column set"); + for (std::uint32_t i = 0; i < columnSetLength; ++i) { + const auto& set = columnSet[i]; + if (set.columns == nullptr || set.columns_length <= 0) throw std::invalid_argument(std::string(funcName) + ": empty column set"); + if (!IsConnector(set.logical_connector)) throw std::invalid_argument(std::string(funcName) + ": unknown logical connector " + std::to_string(set.logical_connector)); + for (std::int32_t j = 0; j < set.columns_length; ++j) { + const auto& column = set.columns[j]; + if (!IsContentColumn(column.column_type)) throw std::invalid_argument(std::string(funcName) + ": unknown column " + std::to_string(column.column_type)); + if (column.column_operator < 0 || column.column_operator > 5) throw std::invalid_argument(std::string(funcName) + ": unknown column operator " + std::to_string(column.column_operator)); + if (column.value == nullptr) throw std::invalid_argument(std::string(funcName) + ": null column value"); + if (!IsConnector(column.logical_connector)) throw std::invalid_argument(std::string(funcName) + ": unknown logical connector " + std::to_string(column.logical_connector)); + } + } +} + +void ValidateOrderBy(const char* funcName, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength) { + if (orderByConditions == nullptr && orderByConditionsLength != 0) throw std::invalid_argument(std::string(funcName) + ": null order-by conditions"); + for (std::uint32_t i = 0; i < orderByConditionsLength; ++i) { + const auto& condition = orderByConditions[i]; + if (!IsSortColumn(condition.column_type)) throw std::invalid_argument(std::string(funcName) + ": unknown sort column " + std::to_string(condition.column_type)); + if (condition.sort_type != 0 && condition.sort_type != 1) throw std::invalid_argument(std::string(funcName) + ": unknown sort type " + std::to_string(condition.sort_type)); + } +} + +[[noreturn]] void ThrowUnknownMetadataId(const char* funcName, std::int32_t metadataId) { + throw std::logic_error(std::string(funcName) + ": metadata id " + std::to_string(metadataId) + " was never opened, as the host content library is empty, and the console's error for it is unknown"); +} } extern "C" { @@ -19,48 +69,69 @@ int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { return 0; } -int APS5_VABI sceContentSearchCloseMetadata(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchGetMetadataFieldInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (field == nullptr || metadataType == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchGetMetadataValue(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (field == nullptr || value == nullptr) APS5_INVALID_ARG_EX; + ThrowUnknownMetadataId(__func__, metadataId); } -int APS5_VABI sceContentSearchOpenMetadata(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (filePath == nullptr || metadataId == nullptr) APS5_INVALID_ARG_EX; + throw std::logic_error(std::string(__func__) + ": " + filePath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } -int APS5_VABI sceContentSearchSearchContent(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceContentSearchGetContentLastUpdateId(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId) { + (void)offset; + if (!g_initialized.load()) return ErrorNotInitialized; + if (numOfContent == nullptr || lastUpdateId == nullptr || (infos == nullptr && limit != 0)) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + ValidateOrderBy(__func__, orderByConditions, orderByConditionsLength); + if (limit > MaxSearchLimit) return ErrorLimitTooBig; + *numOfContent = 0; + *lastUpdateId = LibraryUpdateId; return 0; } -int APS5_VABI sceContentSearchOpenMetadataByContentId(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + *lastUpdateId = LibraryUpdateId; return 0; } -int APS5_VABI sceContentSearchGetTotalContentSize(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (metadataId == nullptr) APS5_INVALID_ARG_EX; + throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); +} + +int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (size == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + *size = 0; + *lastUpdateId = LibraryUpdateId; return 0; } -int APS5_VABI sceContentSearchGetNumOfContent(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId) { + if (!g_initialized.load()) return ErrorNotInitialized; + if (numOfContent == nullptr || lastUpdateId == nullptr) APS5_INVALID_ARG_EX; + ValidateColumnSets(__func__, columnSet, columnSetLength); + *numOfContent = 0; + *lastUpdateId = LibraryUpdateId; return 0; } diff --git a/core/libs/prx/libSceDbgThreadSanitizer/Export.cpp b/core/libs/prx/libSceDbgThreadSanitizer/Export.cpp new file mode 100644 index 0000000000..f2bbed9a32 --- /dev/null +++ b/core/libs/prx/libSceDbgThreadSanitizer/Export.cpp @@ -0,0 +1,14 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +void APS5_VABI __tsan_acquire(void*) { + NotImplemented_nid_no_patch(__func__); +} + +void APS5_VABI __tsan_release(void*) { + NotImplemented_nid_no_patch(__func__); +} + +} diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 5935f1bad8..51df883d95 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include #include #include #include @@ -15,6 +16,9 @@ #ifdef _WIN32 #include #include "prx/libc/include/GuestArena.hpp" +#else +#include +#include #endif static constexpr int32_t SCE_OK = 0; @@ -29,6 +33,7 @@ static constexpr std::size_t FIBER_OBJECT_SIZE = 0x100; static constexpr std::size_t FIBER_OPT_PARAM_SIZE = 0x80; static constexpr std::size_t FIBER_MIN_CONTEXT_SIZE = 512; static constexpr std::uint64_t FIBER_CONTEXT_FILL = 0xdeadbeefdeadbeefull; +static constexpr std::size_t FIBER_HOST_STACK_MARGIN = 1u << 20; using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); @@ -53,6 +58,8 @@ struct Fiber { void* savedStack; char name[FIBER_MAX_NAME_LENGTH + 1]; bool contextSizeCheck; + std::uint8_t* hostStack; + std::uint64_t hostStackBytes; }; static_assert(sizeof(Fiber) <= FIBER_OBJECT_SIZE, "guest reserves 0x100 bytes for SceFiber"); @@ -259,10 +266,84 @@ static void UnpinStack(const void*, std::uint64_t) {} #endif +#ifdef _WIN32 + +static std::uint64_t HostStackGuardBytes() { + static const std::uint64_t bytes = [] { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + return static_cast(system.dwPageSize); + }(); + return bytes; +} + +static void AllocateHostStack(Fiber* fiber) { + const auto page = HostStackGuardBytes(); + const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; + auto* mapping = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (mapping == nullptr) throw std::runtime_error(std::string("sceFiberInitialize: no host stack of ") + std::to_string(bytes) + " bytes for fiber '" + fiber->name + "' (error " + std::to_string(GetLastError()) + ")"); + DWORD previous = 0; + if (!VirtualProtect(mapping, page, PAGE_NOACCESS, &previous)) { + const auto error = GetLastError(); + VirtualFree(mapping, 0, MEM_RELEASE); + throw std::runtime_error(std::string("sceFiberInitialize: cannot protect the guard page of the host stack of fiber '") + fiber->name + "' (error " + std::to_string(error) + ")"); + } + fiber->hostStack = mapping; + fiber->hostStackBytes = bytes; +} + +static void ReleaseHostStack(Fiber* fiber) { + if (fiber->hostStack && !VirtualFree(fiber->hostStack, 0, MEM_RELEASE)) throw std::runtime_error(std::string("sceFiberFinalize: cannot release the host stack of fiber '") + fiber->name + "' (error " + std::to_string(GetLastError()) + ")"); + fiber->hostStack = nullptr; + fiber->hostStackBytes = 0; +} + +#else + +static void AllocateHostStack(Fiber* fiber) { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; + void* mapping = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0); + if (mapping == MAP_FAILED) throw std::runtime_error(std::string("sceFiberInitialize: no host stack of ") + std::to_string(bytes) + " bytes for fiber '" + fiber->name + "' (errno " + std::to_string(errno) + ")"); + if (mprotect(mapping, page, PROT_NONE) != 0) { + const int error = errno; + munmap(mapping, bytes); + throw std::runtime_error(std::string("sceFiberInitialize: cannot protect the guard page of the host stack of fiber '") + fiber->name + "' (errno " + std::to_string(error) + ")"); + } + fiber->hostStack = static_cast(mapping); + fiber->hostStackBytes = bytes; +} + +static void ReleaseHostStack(Fiber* fiber) { + if (fiber->hostStack) munmap(fiber->hostStack, fiber->hostStackBytes); + fiber->hostStack = nullptr; + fiber->hostStackBytes = 0; +} + +#endif + +static std::uint8_t* StackTop(const Fiber* fiber) { + return fiber->hostStack ? fiber->hostStack + fiber->hostStackBytes : fiber->context + fiber->contextSize; +} + +static std::uint64_t* SizedWindow(const Fiber* fiber) { + return reinterpret_cast(StackTop(fiber) - fiber->contextSize); +} + +#ifdef _WIN32 + +static StackBounds FiberBounds(const Fiber* fiber) { + return {fiber->hostStack + fiber->hostStackBytes, fiber->hostStack + HostStackGuardBytes(), fiber->hostStack}; +} + +#else + static StackBounds FiberBounds(const Fiber* fiber) { return {fiber->context + fiber->contextSize, fiber->context, fiber->context}; } +#endif + static Fiber* AsFiber(FiberObject* object) { auto* fiber = reinterpret_cast(object); return fiber && fiber->magic == FIBER_MAGIC ? fiber : nullptr; @@ -275,7 +356,7 @@ extern "C" [[noreturn]] void Aps5FiberMain_nid_no_patch(Fiber* fiber) { } static void PrepareInitialStack(Fiber* fiber) { - const auto top = reinterpret_cast(fiber->context + fiber->contextSize) & ~static_cast(15); + const auto top = reinterpret_cast(StackTop(fiber)) & ~static_cast(15); auto* frame = reinterpret_cast(top - 256); std::memset(frame, 0, sizeof(*frame)); frame->mxcsr = _mm_getcsr(); @@ -330,7 +411,10 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const if (addr_context == nullptr) return SCE_FIBER_ERROR_INVALID; if (size_context < FIBER_MIN_CONTEXT_SIZE) return SCE_FIBER_ERROR_RANGE; auto* fiber = reinterpret_cast(object); - if (fiber->magic == FIBER_MAGIC) UnpinStack(fiber->context, fiber->contextSize); + if (fiber->magic == FIBER_MAGIC) { + UnpinStack(fiber->context, fiber->contextSize); + ReleaseHostStack(fiber); + } std::memset(object, 0, FIBER_OBJECT_SIZE); fiber->magic = FIBER_MAGIC; fiber->state.store(FiberState::Idle, std::memory_order_relaxed); @@ -339,9 +423,10 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const fiber->context = static_cast(addr_context); fiber->contextSize = size_context; std::strncpy(fiber->name, name, FIBER_MAX_NAME_LENGTH); + AllocateHostStack(fiber); fiber->contextSizeCheck = g_contextSizeCheck.load(std::memory_order_relaxed); if (fiber->contextSizeCheck) { - auto* words = static_cast(addr_context); + auto* words = SizedWindow(fiber); std::fill(words, words + size_context / sizeof(std::uint64_t), FIBER_CONTEXT_FILL); } PinStack(addr_context, size_context); @@ -356,6 +441,7 @@ int32_t APS5_VABI sceFiberFinalize(FiberObject* object) { if (state == FiberState::Running || state == FiberState::Suspending) return SCE_FIBER_ERROR_STATE; fiber->magic = 0; UnpinStack(fiber->context, fiber->contextSize); + ReleaseHostStack(fiber); return SCE_OK; } @@ -430,7 +516,7 @@ int32_t APS5_VABI sceFiberGetInfo(FiberObject* object, FiberInfo* fiber_info) { std::memcpy(fiber_info->name, fiber->name, sizeof(fiber_info->name)); fiber_info->size_context_margin = static_cast(-1); if (fiber->contextSizeCheck) { - const auto* words = reinterpret_cast(fiber->context); + const auto* words = SizedWindow(fiber); const auto* end = words + fiber->contextSize / sizeof(std::uint64_t); fiber_info->size_context_margin = static_cast(std::find_if(words, end, [](std::uint64_t word) { return word != FIBER_CONTEXT_FILL; }) - words) * sizeof(std::uint64_t); } diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index a6ca6f3ad6..9f1c4cb8f9 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -11,6 +11,11 @@ constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast") + endif() add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_fstatvfs_conversion_tests) @@ -20,6 +31,15 @@ if(BUILD_TESTING) set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fdopendir_tests ${LIBS_INCLUDE_DIR}/tests/GuestFdopendir.cpp) + target_include_directories(guest_fdopendir_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fdopendir_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_fdopendir_tests) + add_test(NAME guest_fdopendir COMMAND guest_fdopendir_tests) + if(WIN32) + set_tests_properties(guest_fdopendir PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp) target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_utime_forwarding_tests) @@ -35,6 +55,16 @@ if(BUILD_TESTING) set_tests_properties(guest_utime PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_posix_spawn_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSpawn.cpp) + target_include_directories(guest_posix_spawn_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_spawn_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_posix_spawn_tests) + add_test(NAME guest_posix_spawn COMMAND guest_posix_spawn_tests) + set_tests_properties(guest_posix_spawn PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_posix_spawn PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_program_name_tests ${LIBS_INCLUDE_DIR}/tests/GuestProgramName.cpp) target_include_directories(guest_program_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_program_name_tests PRIVATE libSceLibcInternal libkernel libc) @@ -55,6 +85,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getopt_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetopt.cpp) + target_include_directories(guest_getopt_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getopt_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_getopt_tests) + add_test(NAME guest_getopt COMMAND guest_getopt_tests) + set_tests_properties(guest_getopt PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getopt PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) @@ -65,4 +105,14 @@ if(BUILD_TESTING) set_tests_properties(guest_force_tls_destructor PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fminf_tests ${LIBS_INCLUDE_DIR}/tests/GuestFminf.cpp) + target_include_directories(guest_fminf_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fminf_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_fminf_tests) + add_test(NAME guest_fminf COMMAND guest_fminf_tests) + set_tests_properties(guest_fminf PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fminf PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibcInternal/Directory.cpp b/core/libs/prx/libSceLibcInternal/Directory.cpp new file mode 100644 index 0000000000..937f52fd97 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Directory.cpp @@ -0,0 +1,16 @@ +#include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include + +extern "C" int APS5_VABI close_nid_postfix(int d); +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" void* APS5_VABI fdopendir_nid_postfix(int fd) { + int error = 0; + const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (path == nullptr) { + *__error_nid_postfix() = error == EBADF ? 9 : 20; + return nullptr; + } + return OpenDescriptorDirectory_nid_no_patch(path, fd, close_nid_postfix); +} diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 567f6271cf..8356a4cb52 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef _WIN32 #include #else @@ -141,4 +142,13 @@ unsigned int APS5_VABI alarm_nid_postfix(unsigned int seconds) { return GuestAlarm_nid_no_patch(seconds); } +float APS5_VABI fminf_nid_postfix(float x, float y) { + if (std::isnan(x)) + return y; + if (std::isnan(y)) + return x; + if (std::signbit(x) != std::signbit(y)) + return std::signbit(x) ? x : y; + return x < y ? x : y; +} } diff --git a/core/libs/prx/libSceLibcInternal/Getopt.cpp b/core/libs/prx/libSceLibcInternal/Getopt.cpp new file mode 100644 index 0000000000..17062904ff --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Getopt.cpp @@ -0,0 +1,358 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestOption { + const char* name; + int hasArgument; + int* flag; + int value; +}; +static_assert(sizeof(GuestOption) == 32); +static_assert(offsetof(GuestOption, hasArgument) == 8); +static_assert(offsetof(GuestOption, flag) == 16); +static_assert(offsetof(GuestOption, value) == 24); + +extern "C" { +extern const char* __progname_nid_postfix; +extern FileStream* __stderrp_nid_postfix; +char* APS5_VABI getenv_nid_postfix(const char* name); + +int opterr_nid_postfix = 1; +int optind_nid_postfix = 1; +int optopt_nid_postfix = 0; +int optreset_nid_postfix = 0; +char* optarg_nid_postfix = nullptr; +} + +namespace { + +constexpr int NoArgument = 0; +constexpr int RequiredArgument = 1; +constexpr int OptionalArgument = 2; + +constexpr int FlagPermute = 1; +constexpr int FlagAllArguments = 2; + +constexpr int BadCharacter = '?'; +constexpr int InOrder = 1; + +char g_emptyMessage[1] {}; +char* g_shortPlace = g_emptyMessage; +char* g_longPlace = g_emptyMessage; +int g_nonOptionStart = -1; +int g_nonOptionEnd = -1; +int g_posixlyCorrect = -1; + +void Warn(std::string_view message) { + const char* const name = __progname_nid_postfix ? __progname_nid_postfix : "(null)"; + std::string line(name); + line.append(": ").append(message).append("\n"); + FileStream* const stream = __stderrp_nid_postfix; + std::fwrite(line.data(), 1, line.size(), GetNativeStream(stream)); + stream->SyncStatus(); +} + +std::string Character(int character) { + return std::string(1, static_cast(character)); +} + +char* Argument(char* const* arguments, int index, const char* function) { + if (!arguments) throw std::invalid_argument(std::string(function) + ": null argument vector"); + if (index < 0) throw std::invalid_argument(std::string(function) + ": negative optind " + std::to_string(index)); + char* const argument = arguments[index]; + if (!argument) throw std::invalid_argument(std::string(function) + ": null argument " + std::to_string(index)); + return argument; +} + +int OptionCharacter(const char* place) { + return static_cast(*place); +} + +bool PrintError(const char* options) { + return opterr_nid_postfix && *options != ':'; +} + +int BadArgument(const char* options) { + return *options == ':' ? ':' : '?'; +} + +int Gcd(int a, int b) { + int c = a % b; + while (c != 0) { + a = b; + b = c; + c = a % b; + } + return b; +} + +void PermuteArguments(int nonOptionStart, int nonOptionEnd, int optionEnd, char* const* arguments) { + char** const vector = const_cast(arguments); + const int nonOptions = nonOptionEnd - nonOptionStart; + const int options = optionEnd - nonOptionEnd; + if (nonOptions <= 0 || options <= 0) throw std::logic_error("getopt_long: optind moved inside a permutation"); + const int cycles = Gcd(nonOptions, options); + const int cycleLength = (optionEnd - nonOptionStart) / cycles; + for (int i = 0; i < cycles; ++i) { + const int cycleStart = nonOptionEnd + i; + int position = cycleStart; + for (int j = 0; j < cycleLength; ++j) { + if (position >= nonOptionEnd) position -= nonOptions; + else position += options; + char* const swap = vector[position]; + vector[position] = vector[cycleStart]; + vector[cycleStart] = swap; + } + } +} + +int ParseLongOptions(char* const* arguments, const char* options, const GuestOption* longOptions, int* index, const char* dash) { + char* const current = g_longPlace; + int match = -1; + bool exactMatch = false; + bool secondPartialMatch = false; + + ++optind_nid_postfix; + + char* hasEqual = std::strchr(current, '='); + std::size_t length; + if (hasEqual) { + length = static_cast(hasEqual - current); + ++hasEqual; + } else { + length = std::strlen(current); + } + + for (int i = 0; longOptions[i].name; ++i) { + if (std::strncmp(current, longOptions[i].name, length) != 0) continue; + if (std::strlen(longOptions[i].name) == length) { + match = i; + exactMatch = true; + break; + } + if (match == -1) match = i; + else if (longOptions[i].hasArgument != longOptions[match].hasArgument || + longOptions[i].flag != longOptions[match].flag || + longOptions[i].value != longOptions[match].value) + secondPartialMatch = true; + } + const std::string_view name(current, length); + if (!exactMatch && secondPartialMatch) { + if (PrintError(options)) Warn(std::string("option `") + dash + std::string(name) + "' is ambiguous"); + optopt_nid_postfix = 0; + return BadCharacter; + } + if (match == -1) { + if (PrintError(options)) Warn(std::string("unrecognized option `") + dash + current + "'"); + optopt_nid_postfix = 0; + return BadCharacter; + } + const GuestOption& option = longOptions[match]; + if (option.hasArgument == NoArgument && hasEqual) { + if (PrintError(options)) Warn(std::string("option `") + dash + std::string(name) + "' doesn't allow an argument"); + optopt_nid_postfix = option.flag ? 0 : option.value; + return BadCharacter; + } + if (option.hasArgument == RequiredArgument || option.hasArgument == OptionalArgument) { + if (hasEqual) optarg_nid_postfix = hasEqual; + else if (option.hasArgument == RequiredArgument) optarg_nid_postfix = arguments[optind_nid_postfix++]; + } + if (option.hasArgument == RequiredArgument && !optarg_nid_postfix) { + if (PrintError(options)) Warn(std::string("option `") + dash + current + "' requires an argument"); + optopt_nid_postfix = option.flag ? 0 : option.value; + --optind_nid_postfix; + return BadArgument(options); + } + if (index) *index = match; + if (option.flag) { + *option.flag = option.value; + return 0; + } + return option.value; +} + +int GetoptInternal(int count, char* const* arguments, const char* options, const GuestOption* longOptions, int* index, int flags) { + if (!options) return -1; + + if (optind_nid_postfix == 0) optind_nid_postfix = optreset_nid_postfix = 1; + + if (g_posixlyCorrect == -1 || optreset_nid_postfix) + g_posixlyCorrect = getenv_nid_postfix("POSIXLY_CORRECT") != nullptr; + if (*options == '-') flags |= FlagAllArguments; + else if (g_posixlyCorrect || *options == '+') flags &= ~FlagPermute; + if (*options == '+' || *options == '-') ++options; + + optarg_nid_postfix = nullptr; + if (optreset_nid_postfix) g_nonOptionStart = g_nonOptionEnd = -1; + + while (optreset_nid_postfix || !*g_longPlace) { + optreset_nid_postfix = 0; + if (optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + optind_nid_postfix -= g_nonOptionEnd - g_nonOptionStart; + } else if (g_nonOptionStart != -1) { + optind_nid_postfix = g_nonOptionStart; + } + g_nonOptionStart = g_nonOptionEnd = -1; + return -1; + } + g_longPlace = Argument(arguments, optind_nid_postfix, "getopt_long"); + if (*g_longPlace != '-' || g_longPlace[1] == '\0') { + g_longPlace = g_emptyMessage; + if (flags & FlagAllArguments) { + optarg_nid_postfix = arguments[optind_nid_postfix++]; + return InOrder; + } + if (!(flags & FlagPermute)) return -1; + if (g_nonOptionStart == -1) { + g_nonOptionStart = optind_nid_postfix; + } else if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + g_nonOptionStart = optind_nid_postfix - (g_nonOptionEnd - g_nonOptionStart); + g_nonOptionEnd = -1; + } + ++optind_nid_postfix; + continue; + } + if (g_nonOptionStart != -1 && g_nonOptionEnd == -1) g_nonOptionEnd = optind_nid_postfix; + + if (g_longPlace[1] != '\0' && *++g_longPlace == '-' && g_longPlace[1] == '\0') { + ++optind_nid_postfix; + g_longPlace = g_emptyMessage; + if (g_nonOptionEnd != -1) { + PermuteArguments(g_nonOptionStart, g_nonOptionEnd, optind_nid_postfix, arguments); + optind_nid_postfix -= g_nonOptionEnd - g_nonOptionStart; + } + g_nonOptionStart = g_nonOptionEnd = -1; + return -1; + } + break; + } + + if (longOptions && *g_longPlace == '-' && g_longPlace != arguments[optind_nid_postfix]) { + ++g_longPlace; + const int result = ParseLongOptions(arguments, options, longOptions, index, "--"); + g_longPlace = g_emptyMessage; + return result; + } + + const int character = OptionCharacter(g_longPlace++); + const char* found = nullptr; + if (character == ':' || (character == '-' && *g_longPlace != '\0') || + (found = std::strchr(options, character)) == nullptr) { + if (character == '-' && *g_longPlace == '\0') return -1; + if (!*g_longPlace) ++optind_nid_postfix; + if (PrintError(options)) + Warn((g_posixlyCorrect ? "illegal option -- " : "invalid option -- ") + Character(character)); + optopt_nid_postfix = character; + return BadCharacter; + } + if (longOptions && character == 'W' && found[1] == ';') { + if (!*g_longPlace) { + if (++optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (PrintError(options)) Warn("option requires an argument -- " + Character(character)); + optopt_nid_postfix = character; + return BadArgument(options); + } + g_longPlace = Argument(arguments, optind_nid_postfix, "getopt_long"); + } + const int result = ParseLongOptions(arguments, options, longOptions, index, "-W "); + g_longPlace = g_emptyMessage; + return result; + } + if (*++found != ':') { + if (!*g_longPlace) ++optind_nid_postfix; + } else { + optarg_nid_postfix = nullptr; + if (*g_longPlace) { + optarg_nid_postfix = g_longPlace; + } else if (found[1] != ':') { + if (++optind_nid_postfix >= count) { + g_longPlace = g_emptyMessage; + if (PrintError(options)) Warn("option requires an argument -- " + Character(character)); + optopt_nid_postfix = character; + return BadArgument(options); + } + optarg_nid_postfix = arguments[optind_nid_postfix]; + } + g_longPlace = g_emptyMessage; + ++optind_nid_postfix; + } + return character; +} + +} + +extern "C" { + +int APS5_VABI getopt_nid_postfix(int count, char* const* arguments, const char* options) { + if (!options) throw std::invalid_argument("getopt: null option string"); + if (optreset_nid_postfix || *g_shortPlace == '\0') { + optreset_nid_postfix = 0; + if (optind_nid_postfix >= count) { + g_shortPlace = g_emptyMessage; + return -1; + } + g_shortPlace = Argument(arguments, optind_nid_postfix, "getopt"); + if (*g_shortPlace++ != '-') { + g_shortPlace = g_emptyMessage; + return -1; + } + optopt_nid_postfix = OptionCharacter(g_shortPlace++); + if (optopt_nid_postfix == '-' && *g_shortPlace == '\0') { + ++optind_nid_postfix; + g_shortPlace = g_emptyMessage; + return -1; + } + if (optopt_nid_postfix == 0) { + g_shortPlace = g_emptyMessage; + if (!std::strchr(options, '-')) return -1; + optopt_nid_postfix = '-'; + } + } else { + optopt_nid_postfix = OptionCharacter(g_shortPlace++); + } + + const char* found = nullptr; + if (optopt_nid_postfix == ':' || (found = std::strchr(options, optopt_nid_postfix)) == nullptr) { + if (*g_shortPlace == '\0') ++optind_nid_postfix; + if (opterr_nid_postfix && *options != ':') Warn("illegal option -- " + Character(optopt_nid_postfix)); + return BadCharacter; + } + + if (found[1] != ':') { + optarg_nid_postfix = nullptr; + if (*g_shortPlace == '\0') ++optind_nid_postfix; + } else { + if (*g_shortPlace) { + optarg_nid_postfix = g_shortPlace; + } else if (found[2] == ':') { + optarg_nid_postfix = nullptr; + } else if (count > ++optind_nid_postfix) { + optarg_nid_postfix = arguments[optind_nid_postfix]; + } else { + g_shortPlace = g_emptyMessage; + if (*options == ':') return ':'; + if (opterr_nid_postfix) Warn("option requires an argument -- " + Character(optopt_nid_postfix)); + return BadCharacter; + } + g_shortPlace = g_emptyMessage; + ++optind_nid_postfix; + } + return optopt_nid_postfix; +} + +int APS5_VABI getopt_long_nid_postfix(int count, char* const* arguments, const char* options, const GuestOption* longOptions, int* index) { + return GetoptInternal(count, arguments, options, longOptions, index, FlagPermute); +} + +} diff --git a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp new file mode 100644 index 0000000000..a621e90dd2 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -0,0 +1,122 @@ +#include "prx/libc/include/FilesystemError.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +struct LibcStatvfs; +extern "C" int APS5_VABI fstatvfs_nid_postfix(int, LibcStatvfs*); + +namespace { +int Fail(int error) { + errno = error; + return -1; +} + +int HostError(int error) { + switch (error) { + case ENOENT: return 2; + case EINTR: return 4; + case EIO: return 5; + case ENOMEM: return 12; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EAGAIN: return 35; + case ELOOP: return 62; + case ENAMETOOLONG: return 63; + default: throw std::system_error(error, std::generic_category(), "statvfs: unsupported host error"); + } +} + +struct Descriptor { + int value; + ~Descriptor() { + const int saved = errno; +#ifdef _WIN32 + ::_close(value); +#else + ::close(value); +#endif + errno = saved; + } +}; + +int OpenMetadata(const std::filesystem::path& path, bool directory) { +#ifdef _WIN32 + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD error = GetLastError(); + if (error == ERROR_PATH_NOT_FOUND || error == ERROR_INVALID_NAME || error == ERROR_DIRECTORY) { + for (auto prefix = path.parent_path(); !prefix.empty();) { + const DWORD attributes = GetFileAttributesW(prefix.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) return Fail(20); + const auto parent = prefix.parent_path(); + if (parent == prefix) break; + prefix = parent; + } + } + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: return Fail(2); + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: return Fail(13); + case ERROR_DIRECTORY: return Fail(20); + case ERROR_INVALID_NAME: + case ERROR_INVALID_PARAMETER: return Fail(22); + case ERROR_CANT_RESOLVE_FILENAME: return Fail(62); + case ERROR_FILENAME_EXCED_RANGE: return Fail(63); + default: throw std::system_error(error, std::system_category(), "statvfs: CreateFileW"); + } + } + if (directory) { + BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(handle, &info)) { + const DWORD error = GetLastError(); + CloseHandle(handle); + throw std::system_error(error, std::system_category(), "statvfs: GetFileInformationByHandle"); + } + if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + CloseHandle(handle); + return Fail(20); + } + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY | _O_NOINHERIT); + if (descriptor < 0) { + const int error = errno; + CloseHandle(handle); + return Fail(HostError(error)); + } + return descriptor; +#else + const int descriptor = ::open(path.c_str(), O_PATH | O_CLOEXEC | (directory ? O_DIRECTORY : 0)); + return descriptor < 0 ? Fail(HostError(errno)) : descriptor; +#endif +} +} + +extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { + if (path == nullptr) return Fail(14); + if (*path == '\0') return Fail(2); + const int saved = errno; + const auto last = std::string_view(path).back(); + const bool directory = last == '/' || last == '\\' || FinalPathComponentIsDot_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); + const int descriptor = OpenMetadata(native, directory); + if (descriptor < 0) return -1; + const Descriptor owner{descriptor}; + errno = saved; + return fstatvfs_nid_postfix(descriptor, output); +} diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp new file mode 100644 index 0000000000..c4571fb64e --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -0,0 +1,119 @@ +#include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +namespace { + +constexpr int BadDescriptor = 9; +constexpr int OutOfMemory = 12; + +PosixSpawnAttributes& Attributes(PosixSpawnAttributes* const* attributes, const char* function) { + if (attributes == nullptr || *attributes == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized attributes"); + return **attributes; +} + +PosixSpawnFileActions& FileActions(PosixSpawnFileActions* const* actions, const char* function) { + if (actions == nullptr || *actions == nullptr) throw std::invalid_argument(std::string(function) + ": uninitialized file actions"); + return **actions; +} + +int Append(PosixSpawnFileActions& actions, PosixSpawnFileAction action) { + try { + actions.actions.push_back(std::move(action)); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } + return 0; +} + +} + +extern "C" { + +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_init: null attributes"); + auto* created = new (std::nothrow) PosixSpawnAttributes{}; + if (created == nullptr) return OutOfMemory; + *attributes = created; + return 0; +} + +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes** attributes) { + if (attributes == nullptr) throw std::invalid_argument("posix_spawnattr_destroy: null attributes"); + delete *attributes; + return 0; +} + +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes** attributes, std::int16_t flags) { + Attributes(attributes, "posix_spawnattr_setflags").flags = flags; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalDefault) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_setsigdefault: null signal set"); + target.signalDefault = *signalDefault; + return 0; +} + +int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions** actions) { + if (actions == nullptr) throw std::invalid_argument("posix_spawn_file_actions_init: null file actions"); + auto* created = new (std::nothrow) PosixSpawnFileActions{}; + if (created == nullptr) return -1; + *actions = created; + return 0; +} + +int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions** actions) { + delete &FileActions(actions, "posix_spawn_file_actions_destroy"); + return 0; +} + +int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions** actions, int descriptor, const char* path, int openFlags, std::uint16_t mode) { + if (descriptor < 0) return BadDescriptor; + if (path == nullptr) throw std::invalid_argument("posix_spawn_file_actions_addopen: null path"); + auto& target = FileActions(actions, "posix_spawn_file_actions_addopen"); + try { + return Append(target, {PosixSpawnFileActionKind::Open, descriptor, path, openFlags, mode, 0}); + } catch (const std::bad_alloc&) { + return OutOfMemory; + } +} + +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions** actions, int descriptor, int newDescriptor) { + if (descriptor < 0 || newDescriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_adddup2"), {PosixSpawnFileActionKind::Duplicate, descriptor, {}, 0, 0, newDescriptor}); +} + +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions** actions, int descriptor) { + if (descriptor < 0) return BadDescriptor; + return Append(FileActions(actions, "posix_spawn_file_actions_addclose"), {PosixSpawnFileActionKind::Close, descriptor, {}, 0, 0, 0}); +} + +int APS5_VABI posix_spawn_nid_postfix(std::int32_t* processId, const char* path, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { + (void)processId; + (void)path; + (void)actions; + (void)attributes; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("posix_spawn: process creation"); + return -1; +} + +int APS5_VABI posix_spawnp_nid_postfix(std::int32_t* processId, const char* file, PosixSpawnFileActions* const* actions, PosixSpawnAttributes* const* attributes, char* const* arguments, char* const* environment) { + (void)processId; + (void)file; + (void)actions; + (void)attributes; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("posix_spawnp: process creation"); + return -1; +} + +} diff --git a/core/libs/prx/libSceLibcInternal/include/Spawn.hpp b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp new file mode 100644 index 0000000000..5e93fa8159 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/include/Spawn.hpp @@ -0,0 +1,37 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP +#define CORE_LIBS_PRX_LIBSCELIBCINTERNAL_INCLUDE_SPAWN_HPP + +#include +#include +#include + +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +static_assert(sizeof(GuestSignalSet) == 16); + +struct PosixSpawnAttributes { + std::int16_t flags; + std::int32_t processGroup; + std::int32_t schedulingPriority; + std::int32_t schedulingPolicy; + GuestSignalSet signalDefault; + GuestSignalSet signalMask; +}; + +enum class PosixSpawnFileActionKind { Open, Duplicate, Close }; + +struct PosixSpawnFileAction { + PosixSpawnFileActionKind kind; + int descriptor; + std::string path; + int openFlags; + std::uint16_t mode; + int newDescriptor; +}; + +struct PosixSpawnFileActions { + std::vector actions; +}; + +#endif diff --git a/core/libs/prx/libSceMouse/CMakeLists.txt b/core/libs/prx/libSceMouse/CMakeLists.txt index d98fdf1a54..b7f58761da 100644 --- a/core/libs/prx/libSceMouse/CMakeLists.txt +++ b/core/libs/prx/libSceMouse/CMakeLists.txt @@ -12,7 +12,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/../libSceVideoOut/src/MouseInput.cpp ) target_include_directories(mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) - target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc) + target_link_libraries(mouse_tests PRIVATE libSceMouse libkernel libc SDL2-static) configure_windows_unwind(mouse_tests) add_test(NAME mouse_input COMMAND mouse_tests) if(WIN32) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 157e3afccb..f41c9eaf48 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -27,4 +27,9 @@ int APS5_VABI sceMouseResetAbsolutePosition() { NotImplemented_nid_no_patch(__func__); return 0; } + +bool MouseIsOpen_nid_postfix() { + return Mouse::IsOpen(); +} + } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index bf7d57a09f..15c89831b6 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -16,6 +16,7 @@ struct MouseInputEvent { }; namespace Mouse { +bool IsOpen(); int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); @@ -24,5 +25,6 @@ void Publish(const MouseInputEvent& event); } extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_postfix(); #endif diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index 5bcf78e720..c139fff6a2 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -27,6 +27,11 @@ void enqueue(const MouseData& data) { namespace Mouse { +bool IsOpen() { + std::lock_guard lock(mouseMutex); + return opened; +} + int Initialize() { std::lock_guard lock(mouseMutex); initialized = true; diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 09f46bd6ed..27638ea00f 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 @@ -30,6 +31,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -40,6 +42,7 @@ namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; +constexpr int NET_EINTR = 4; constexpr int NET_EBADF = 9; constexpr int NET_EACCES = 13; constexpr int NET_EFAULT = 14; @@ -66,6 +69,8 @@ constexpr int NET_ECONNREFUSED = 61; constexpr int NET_EHOSTUNREACH = 65; constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); +constexpr int NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION = 0x1; +constexpr int NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION = 0x2; constexpr int NET_AF_INET = 2; constexpr int NET_AF_INET6 = 28; @@ -83,6 +88,8 @@ constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; +constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; +constexpr int NET_SO_SCE_RCVTIMEO = 0x1106; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; constexpr int NET_UIO_MAXIOV = 1024; @@ -221,9 +228,9 @@ bool native_to_guest_address(const sockaddr_storage& native, void* address, std: } else { return false; } - const auto required = static_cast(bytes[0]); - std::memcpy(address, bytes, std::min(*length, required)); - *length = required; + const auto copied = std::min(*length, bytes[0]); + std::memcpy(address, bytes, copied); + *length = copied; return true; } @@ -302,7 +309,14 @@ struct NetMemoryPoolStats { std::size_t max_inuse_size; std::size_t current_inuse_size; }; -std::map g_resolvers; +struct Resolver { + int error = 0; + int running = 0; + std::uint64_t aborts = 0; + bool abort_ntoa = false; + bool abort_aton = false; +}; +std::map g_resolvers; int g_next_sock = 32; int g_next_epoll = 0x4000; int g_next_pool = 1; @@ -365,10 +379,27 @@ int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint return 0; } -void set_resolver_error(int rid, int error) { +int begin_lookup(int rid, bool Resolver::*preservedAbort, std::uint64_t& generation) { std::lock_guard lk(g_mutex); const auto resolver = g_resolvers.find(rid); - if (resolver != g_resolvers.end()) resolver->second = error; + if (resolver == g_resolvers.end()) return fail(NET_EBADF); + if (std::exchange(resolver->second.*preservedAbort, false)) { + resolver->second.error = NET_ERROR_BASE | NET_EINTR; + return fail(NET_EINTR); + } + ++resolver->second.running; + generation = resolver->second.aborts; + return 0; +} + +int end_lookup(int rid, std::uint64_t generation, int error) { + std::lock_guard lk(g_mutex); + const auto resolver = g_resolvers.find(rid); + if (resolver == g_resolvers.end()) return error; + --resolver->second.running; + if (resolver->second.aborts != generation) error = fail(NET_EINTR); + resolver->second.error = error; + return error; } void log_soft(const char* func, const char* what) { @@ -409,6 +440,12 @@ struct NetMsghdr { static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, control) == 32 && offsetof(NetMsghdr, flags) == 44); +int timeout_option(int optname) { + if (optname == NET_SO_SCE_SNDTIMEO) return NET_SO_SNDTIMEO; + if (optname == NET_SO_SCE_RCVTIMEO) return NET_SO_RCVTIMEO; + return optname; +} + std::int64_t message_length(const NetMsghdr* message) { if (!message) return fail(NET_EFAULT); if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); @@ -592,8 +629,9 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) close_socket(native); return fail(12); } + const int fd = KernelReserveSocketNumber_nid_no_patch(static_cast(handle->value)); + if (fd < 0) return fail(-fd); std::lock_guard lk(g_mutex); - const int fd = g_next_sock++; Sock s; s.family = family; s.type = type; @@ -611,6 +649,7 @@ int APS5_VABI sceNetSocketClose(int s) { native = it->second.native; g_socks.erase(it); } + KernelReleaseSocketNumber_nid_no_patch(s); if (native) { #ifdef _WIN32 ::shutdown(native->value, SD_BOTH); @@ -707,10 +746,10 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { connection.family = listener.family; connection.type = NET_SOCK_STREAM; connection.native = std::move(handle); - int descriptor; + const int descriptor = KernelReserveSocketNumber_nid_no_patch(static_cast(connection.native->value)); + if (descriptor < 0) return fail(-descriptor); { std::lock_guard lk(g_mutex); - descriptor = g_next_sock++; g_socks.emplace(descriptor, connection); } if (addr && !native_to_guest_address(peer, addr, addrlen)) { @@ -913,6 +952,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval socket = it->second; } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); + optname = timeout_option(optname); if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; std::memcpy(&value, optval, sizeof(value)); @@ -958,7 +998,7 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; - switch (optname) { + switch (timeout_option(optname)) { case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; @@ -980,8 +1020,15 @@ int APS5_VABI sceNetGetsockname(int s, void* addr, uint32_t* addrlen) { if (!addr || !addrlen) return fail(NET_EINVAL); sockaddr_storage native{}; NativeLength native_length = sizeof(native); - if (::getsockname(socket.native->value, reinterpret_cast(&native), &native_length) != 0) + if (::getsockname(socket.native->value, reinterpret_cast(&native), &native_length) != 0) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return fail(native_error()); + native = {}; + native.ss_family = socket.family == NET_AF_INET ? AF_INET : AF_INET6; +#else return fail(native_error()); +#endif + } return native_to_guest_address(native, addr, addrlen) ? 0 : fail(NET_EAFNOSUPPORT); } @@ -1093,7 +1140,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (const auto& entry : registration->second) { const auto socket = g_socks.find(entry.first); if (socket == g_socks.end()) continue; - registered_events.push_back(entry.second); + registered_events.push_back({entry.second.events, 0, static_cast(entry.first), entry.second.data}); native_sockets.push_back(socket->second.native); } } @@ -1114,14 +1161,14 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int for (std::size_t index = 0; index < descriptors.size(); ++index) { descriptors[index].fd = native_sockets[index]->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLRDNORM; - if (registered_events[index].events & 4) descriptors[index].events |= POLLWRNORM; + if (registered_events[index].events & 2) descriptors[index].events |= POLLWRNORM; } #else std::vector descriptors(registered_events.size()); for (std::size_t index = 0; index < descriptors.size(); ++index) { descriptors[index].fd = native_sockets[index]->value; if (registered_events[index].events & 1) descriptors[index].events |= POLLIN; - if (registered_events[index].events & 4) descriptors[index].events |= POLLOUT; + if (registered_events[index].events & 2) descriptors[index].events |= POLLOUT; } #endif const auto now = std::chrono::steady_clock::now(); @@ -1151,10 +1198,10 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int std::uint32_t guest_events = 0; #ifdef _WIN32 if (native_events & (POLLRDNORM | POLLPRI)) guest_events |= 1; - if (native_events & POLLWRNORM) guest_events |= 4; + if (native_events & POLLWRNORM) guest_events |= 2; #else if (native_events & (POLLIN | POLLPRI)) guest_events |= 1; - if (native_events & POLLOUT) guest_events |= 4; + if (native_events & POLLOUT) guest_events |= 2; #endif if (native_events & POLLERR) guest_events |= 8; if (native_events & POLLHUP) guest_events |= 16; @@ -1244,7 +1291,7 @@ int APS5_VABI sceNetResolverCreate(const char* name, int memid, int flags) { (void)flags; std::lock_guard lk(g_mutex); const int id = g_next_resolver++; - g_resolvers[id] = 0; + g_resolvers[id] = {}; return id; } @@ -1253,28 +1300,42 @@ 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) { - { - std::lock_guard lk(g_mutex); - if (g_resolvers.count(rid) == 0) return fail(NET_EBADF); - } +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) { set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); log_soft(func, "host DNS lookup failed"); - set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); - return NET_ERROR_RESOLVER_ENODNS; + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } for (const addrinfo* entry = results; entry != nullptr; entry = entry->ai_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); - set_resolver_error(rid, 0); + 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; } @@ -1289,6 +1350,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]; @@ -1305,14 +1377,11 @@ struct NetResolverInfo { static_assert(sizeof(NetResolverRecord) == 32 && offsetof(NetResolverRecord, family) == 16); static_assert(sizeof(NetResolverInfo) == 384 && offsetof(NetResolverInfo, count) == 320 && offsetof(NetResolverInfo, count4) == 324); -int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* hostname, NetResolverInfo* info, int timeout, - int retry, int flags) { - (void)timeout; - (void)retry; - if (flags != 0) throw std::runtime_error("sceNetResolverStartNtoaMultipleRecordsEx: flags " + std::to_string(flags) + " are not supported"); +int lookup_records(int rid, const char* hostname, NetResolverInfo* info, int flags, const char* func) { + if (flags != 0) throw std::runtime_error(std::string(func) + ": flags " + std::to_string(flags) + " are not supported"); if (!hostname || !info) return fail(NET_EINVAL); std::vector addresses; - if (const int error = lookup_ipv4(rid, hostname, __func__, addresses)) return error; + if (const int error = lookup_ipv4(rid, hostname, func, addresses)) return error; *info = {}; info->count = static_cast(std::min(addresses.size(), std::size(info->records))); info->count4 = info->count; @@ -1323,28 +1392,41 @@ int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* host return 0; } +int APS5_VABI sceNetResolverStartNtoaMultipleRecords(int rid, const char* hostname, NetResolverInfo* info, int timeout, + int retry, int flags) { + (void)timeout; + (void)retry; + return lookup_records(rid, hostname, info, flags, __func__); +} + +int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int rid, const char* hostname, NetResolverInfo* info, int timeout, + int retry, int flags) { + (void)timeout; + (void)retry; + return lookup_records(rid, hostname, info, flags, __func__); +} + int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, int len, int timeout, int retry, int flags) { (void)timeout; (void)retry; (void)flags; if (!addr || !hostname || len <= 0) return fail(NET_EINVAL); - { - std::lock_guard lk(g_mutex); - if (g_resolvers.count(rid) == 0) return fail(NET_EBADF); - } if (!initialize_sockets()) return fail(5); + std::uint64_t generation = 0; + if (const int error = begin_lookup(rid, &Resolver::abort_aton, generation)) return error; sockaddr_in address{}; address.sin_family = AF_INET; std::memcpy(&address.sin_addr, addr, sizeof(address.sin_addr)); - const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), hostname, + std::vector name(static_cast(len)); + const int result = ::getnameinfo(reinterpret_cast(&address), sizeof(address), name.data(), static_cast(len), nullptr, 0, NI_NAMEREQD); if (result != 0) { set_errno(result == EAI_AGAIN ? NET_ETIMEDOUT : NET_ENOENT); log_soft(__func__, "host reverse DNS lookup failed"); - set_resolver_error(rid, NET_ERROR_RESOLVER_ENODNS); - return NET_ERROR_RESOLVER_ENODNS; + return end_lookup(rid, generation, NET_ERROR_RESOLVER_ENODNS); } - set_resolver_error(rid, 0); + if (const int error = end_lookup(rid, generation, 0)) return error; + std::memcpy(hostname, name.data(), std::strlen(name.data()) + 1); return 0; } @@ -1353,17 +1435,23 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { std::lock_guard lk(g_mutex); const auto resolver = g_resolvers.find(rid); if (resolver == g_resolvers.end()) return fail(NET_EBADF); - *status = resolver->second; + *status = resolver->second.error; return 0; } -int APS5_VABI sceNetResolverAbort(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNetResolverStartNtoaMultipleRecords() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNetResolverAbort(int rid, int flags) { + constexpr int preservation = NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION | NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION; + std::lock_guard lk(g_mutex); + const auto resolver = g_resolvers.find(rid); + if (resolver == g_resolvers.end()) return fail(NET_EBADF); + if ((flags & ~preservation) != 0) throw std::runtime_error("sceNetResolverAbort: flags " + std::to_string(flags) + " are not supported"); + if (resolver->second.running != 0) { + ++resolver->second.aborts; + resolver->second.error = NET_ERROR_BASE | NET_EINTR; + return 0; + } + if ((flags & NET_RESOLVER_ABORT_FLAG_NTOA_PRESERVATION) != 0) resolver->second.abort_ntoa = true; + if ((flags & NET_RESOLVER_ABORT_FLAG_ATON_PRESERVATION) != 0) resolver->second.abort_aton = true; return 0; } diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 4bae507609..5f0ab11e26 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -7,12 +7,18 @@ #include #include #include +#ifdef __linux__ +#include +#endif #endif #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include +#include +#include #ifdef _WIN32 #include #endif @@ -78,12 +84,56 @@ static bool HostHasAddress(int family) { #endif } +extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { + std::ifstream file(path); + std::uint64_t value = 0; + if (!(file >> value)) throw std::runtime_error(std::string("sceNetCtlGetIfStat: no byte counter in ") + path); + return value; +} + +static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { + sent = 0; + received = 0; +#ifdef _WIN32 + MIB_IF_TABLE2* table = nullptr; + if (GetIfTable2(&table) != NO_ERROR) throw std::runtime_error("sceNetCtlGetIfStat: the host interface table is unavailable"); + for (ULONG index = 0; index < table->NumEntries; ++index) { + const auto& row = table->Table[index]; + if (row.OperStatus != IfOperStatusUp || row.Type == IF_TYPE_SOFTWARE_LOOPBACK || !row.InterfaceAndOperStatusFlags.HardwareInterface) continue; + sent += row.OutOctets; + received += row.InOctets; + } + FreeMibTable(table); +#elif defined(__linux__) + ifaddrs* interfaces = nullptr; + if (getifaddrs(&interfaces) != 0) throw std::runtime_error("sceNetCtlGetIfStat: the host interface list is unavailable"); + for (auto* item = interfaces; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_PACKET) continue; + if (!(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + const std::string statistics = std::string("/sys/class/net/") + item->ifa_name + "/statistics/"; + sent += NetCtlReadByteCounter_nid_no_patch((statistics + "tx_bytes").c_str()); + received += NetCtlReadByteCounter_nid_no_patch((statistics + "rx_bytes").c_str()); + } + freeifaddrs(interfaces); +#else + throw std::runtime_error("sceNetCtlGetIfStat: host interface byte counters are not implemented on this platform"); +#endif +} + extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { return 0; } +int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { + if (!stat) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (!HostHasAddress(AF_UNSPEC)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + *stat = {}; + HostTraffic(stat->tx_bytes, stat->rx_bytes); + return 0; +} + int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { (void)code; (void)info; diff --git a/core/libs/prx/libSceNetCtlApDialog/Export.cpp b/core/libs/prx/libSceNetCtlApDialog/Export.cpp index 0dc6cd822a..e1d2bc4a27 100644 --- a/core/libs/prx/libSceNetCtlApDialog/Export.cpp +++ b/core/libs/prx/libSceNetCtlApDialog/Export.cpp @@ -1,38 +1,78 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +struct SceNetCtlApDialogResult { + std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; + std::uint8_t reserved[36] = {}; +}; + +static_assert(sizeof(SceNetCtlApDialogResult) == 40); + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { int APS5_VABI sceNetCtlApDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; } int APS5_VABI sceNetCtlApDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; } -int APS5_VABI sceNetCtlApDialogOpen(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetCtlApDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceNetCtlApDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceNetCtlApDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); } -int APS5_VABI sceNetCtlApDialogGetResult(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetCtlApDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceNetCtlApDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const SceNetCtlApDialogResult dialogResult{}; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); + return 0; } } 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/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index c5e3624b3e..f8ab93148e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,9 +1,30 @@ +#include #include +#include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" static constexpr float DEFAULT_SOUND_SPEED = 343.0f; +int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { + if (param == nullptr) APS5_INVALID_ARG_EX; + if (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (flags != 0) throw std::runtime_error("NGS2: listener coordinate flags are unsupported"); + if (param->position.x != 0.0f || param->position.y != 0.0f || param->position.z != 0.0f) + throw std::runtime_error("NGS2: listener translation is unsupported"); + if (param->orient_front.x != 0.0f || param->orient_front.y != 0.0f || param->orient_front.z != 1.0f || + param->orient_up.x != 0.0f || param->orient_up.y != 1.0f || param->orient_up.z != 0.0f) + throw std::runtime_error("NGS2: non-default listener orientation is unsupported"); + if (!std::isfinite(param->velocity.x) || !std::isfinite(param->velocity.y) || !std::isfinite(param->velocity.z)) APS5_INVALID_ARG_EX; + if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; + Ngs2GeomListenerWork work{}; + for (std::size_t i = 0; i < 4; i++) work.matrix[i][i] = 1.0f; + work.velocity = param->velocity; + work.sound_speed = param->sound_speed; + *outWork = work; + return SCE_NGS2_OK; +} + int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { if (out_listener_param == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; *out_listener_param = {}; diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 65829bf2fa..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); @@ -204,6 +206,7 @@ float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std:: int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers); int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param); +int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags); int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param); #endif 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/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 9cb4cd441b..1541b64394 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -17,14 +17,6 @@ int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2G return 0; } -int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* out_work, uint32_t flags) { - (void)param; - (void)out_work; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { (void)report_type; (void)handler; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index bb45dc5b2b..d90db41275 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -116,7 +116,10 @@ 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 || 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 +248,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 +284,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 +328,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); @@ -490,6 +502,10 @@ int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_ return Ngs2GeomResetListenerParam(out_listener_param); } +int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { + return Ngs2GeomCalcListener(param, outWork, flags); +} + int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { return Ngs2GeomResetSourceParam(out_source_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/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index ca194eea7f..91a377793b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include static constexpr float SoundSpeed = 343.0f; @@ -49,9 +52,114 @@ static void TestResetSourceParam() { Require(sceNgs2GeomResetSourceParam(nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); } +static Ngs2GeomListenerParam Listener() { + Ngs2GeomListenerParam param{}; + Require(sceNgs2GeomResetListenerParam(¶m) == SCE_NGS2_OK); + return param; +} + +static void RequireMatrix(const Ngs2GeomListenerWork& work, const float (&expected)[4][4]) { + for (std::size_t row = 0; row < 4; row++) { + for (std::size_t column = 0; column < 4; column++) Require(work.matrix[row][column] == expected[row][column]); + } +} + +static void TestDefaultListener() { + auto param = Listener(); + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); + Require(work.velocity.x == 0.0f && work.velocity.y == 0.0f && work.velocity.z == 0.0f); + Require(work.sound_speed == SoundSpeed && work.coordinate == 0); + Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); + + param.velocity = {2.0f, -3.0f, 4.0f}; + param.sound_speed = 1500.0f; + std::memset(&work, 0x5a, sizeof(work)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); + Require(work.velocity.x == 2.0f && work.velocity.y == -3.0f && work.velocity.z == 4.0f); + Require(work.sound_speed == 1500.0f && work.coordinate == 0); + Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); + + param.position = {-0.0f, -0.0f, -0.0f}; + param.orient_front = {-0.0f, -0.0f, 1.0f}; + param.orient_up = {-0.0f, 1.0f, -0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); +} + +static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t flags) { + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + unsigned char before[sizeof(work)]; + std::memcpy(before, &work, sizeof(work)); + bool rejected = false; + try { + sceNgs2GeomCalcListener(param, &work, flags); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected); + Require(std::memcmp(before, &work, sizeof(work)) == 0); +} + +static void TestUnsupportedListener() { + auto param = Listener(); + for (const auto flags : {1u, 2u, 0xffffffffu}) RejectListener(¶m, flags); + for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, &Ngs2GeomListenerParam::orient_up}) { + for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { + for (const float value : {-1.0f, 0.0f, 1.0f, 2.0f, std::numeric_limits::min(), std::numeric_limits::max()}) { + param = Listener(); + auto& field = (param.*vector).*component; + if (field == value) continue; + field = value; + RejectListener(¶m, 0); + } + param = Listener(); + auto& field = (param.*vector).*component; + field = std::nextafter(field, 2.0f); + RejectListener(¶m, 0); + } + } +} + +static void TestInvalidListener() { + auto param = Listener(); + Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + RejectListener(nullptr, 0); + for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + param.sound_speed = value; + RejectListener(¶m, 0); + } + param = Listener(); + param.orient_front = {}; + RejectListener(¶m, 0); + param = Listener(); + param.orient_up = param.orient_front; + RejectListener(¶m, 0); + param = Listener(); + param.orient_up = {}; + RejectListener(¶m, 0); + for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, + &Ngs2GeomListenerParam::orient_up, &Ngs2GeomListenerParam::velocity}) { + for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { + for (const float value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + param = Listener(); + (param.*vector).*component = value; + RejectListener(¶m, 0); + } + } + } +} + int main() { TestLayouts(); TestResetListenerParam(); TestResetSourceParam(); + TestDefaultListener(); + TestUnsupportedListener(); + TestInvalidListener(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 6312953af1..fdc379e560 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -40,6 +40,7 @@ int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); +int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam*, Ngs2GeomListenerWork*, std::uint32_t); int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); } diff --git a/core/libs/prx/libSceNpAuth/CMakeLists.txt b/core/libs/prx/libSceNpAuth/CMakeLists.txt new file mode 100644 index 0000000000..ce6ce6a2f8 --- /dev/null +++ b/core/libs/prx/libSceNpAuth/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libSceNpAuth SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpAuth PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpAuth) + +if(BUILD_TESTING) + add_test_executable(guest_np_auth_authorized_app_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpAuthAuthorizedAppCode.cpp) + target_include_directories(guest_np_auth_authorized_app_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_auth_authorized_app_code_tests PRIVATE libSceNpAuth) + add_test(NAME guest_np_auth_authorized_app_code COMMAND guest_np_auth_authorized_app_code_tests) + if(WIN32) + set_tests_properties(guest_np_auth_authorized_app_code PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpAuth/Export.cpp b/core/libs/prx/libSceNpAuth/Export.cpp index c17210d9f1..94a17e4603 100644 --- a/core/libs/prx/libSceNpAuth/Export.cpp +++ b/core/libs/prx/libSceNpAuth/Export.cpp @@ -1,68 +1,133 @@ -#include -#include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); +static constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT = static_cast(0x80550301); static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); +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 std::atomic g_nextRequest{1}; +namespace { + +struct RequestInfo { + bool isAsync = false; + bool processed = false; + bool aborted = false; + int result = SCE_NP_ERROR_SIGNED_OUT; +}; + +std::mutex g_requestMutex; +std::unordered_map g_requests; +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}; + return reqId; +} + +} extern "C" { int APS5_VABI sceNpAuthAbortRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (!it->second.processed) it->second.aborted = true; return 0; } int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param) { (void)param; - return g_nextRequest.fetch_add(1, std::memory_order_relaxed); + return CreateRequest(true); } int APS5_VABI sceNpAuthCreateRequest(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return CreateRequest(false); } int APS5_VABI sceNpAuthDeleteRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_requestMutex); + g_requests.erase(req_id); return 0; } int APS5_VABI sceNpAuthGetAuthorizedAppCode() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id) { - (void)req_id; (void)issuer_id; - if (!param || !auth_code) return SCE_NP_ERROR_INVALID_ARGUMENT; + if (!param || !auth_code) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (it->second.aborted) { + it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; + return ORBIS_NP_AUTH_ERROR_ABORTED; + } + it->second.processed = true; + if (it->second.isAsync) { + it->second.result = SCE_NP_ERROR_SIGNED_OUT; + return 0; + } return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token) { - (void)req_id; - (void)param; - (void)id_token; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!param || !id_token) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + auto it = g_requests.find(req_id); + if (it == g_requests.end()) return ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; + if (it->second.processed) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + if (it->second.aborted) { + it->second.result = ORBIS_NP_AUTH_ERROR_ABORTED; + return ORBIS_NP_AUTH_ERROR_ABORTED; + } + it->second.processed = true; + if (it->second.isAsync) { + it->second.result = SCE_NP_ERROR_SIGNED_OUT; + return 0; + } + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpAuthPollAsync(int req_id, int* result) { - (void)req_id; - if (result) *result = SCE_NP_ERROR_SIGNED_OUT; + std::lock_guard lock(g_requestMutex); + 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 (result) *result = it->second.result; return NP_POLL_ASYNC_FINISHED; } +int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout) { + (void)resolve_retry; + (void)resolve_timeout; + (void)connect_timeout; + (void)send_timeout; + (void)receive_timeout; + std::lock_guard lock(g_requestMutex); + return g_requests.contains(req_id) ? 0 : ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND; +} + int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result) { - (void)req_id; - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!result) return ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_requestMutex); + 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; + *result = it->second.result; + return NP_POLL_ASYNC_FINISHED; } } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index aecaa928b1..c40dbfd360 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -161,13 +161,17 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { return SCE_NP_ERROR_SIGNED_OUT; } -int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpEntitlementAccessGetPftFlag(uint32_t* flag) { + constexpr int errorParameter = static_cast(0x817D0002); + if (!flag) return errorParameter; + *flag = 0; return 0; } -int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(uint32_t* flag) { + constexpr int errorParameter = static_cast(0x817D0002); + if (!flag) return errorParameter; + *flag = 0; return 0; } diff --git a/core/libs/prx/libSceNpPartner001/CMakeLists.txt b/core/libs/prx/libSceNpPartner001/CMakeLists.txt new file mode 100644 index 0000000000..c339e97d94 --- /dev/null +++ b/core/libs/prx/libSceNpPartner001/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceNpPartner001 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpPartner001 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpPartner001) + +if(BUILD_TESTING) + add_test_executable(guest_np_partner_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpPartner.cpp) + target_include_directories(guest_np_partner_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_partner_tests PRIVATE libSceNpPartner001 libc) + configure_windows_unwind(guest_np_partner_tests) + add_test(NAME guest_np_partner COMMAND guest_np_partner_tests) + if(WIN32) + set_tests_properties(guest_np_partner PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp index 74e81b2df4..9d2e089775 100644 --- a/core/libs/prx/libSceNpPartner001/Export.cpp +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -1,14 +1,23 @@ #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceNpEAAccessInitialize(void) { - NotImplemented_nid_no_patch(__func__); + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); return 0; } int APS5_VABI sceNpEAAccessTerminate(void) { - NotImplemented_nid_no_patch(__func__); + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; } diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index b694abb6f2..d3086ee40a 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -6,6 +6,7 @@ // Peer-to-peer signaling needs the network: contexts exist, but sessions never activate. static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80553303); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND = static_cast(0x8055330C); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); static std::atomic g_nextContext{1}; @@ -60,11 +61,10 @@ int APS5_VABI sceNpSessionSignalingTerminate(void) { int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(int32_t context_id, int32_t connection_id, int32_t* status, void* peer_address, uint16_t* peer_port) { (void)context_id; (void)connection_id; - (void)status; (void)peer_address; (void)peer_port; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!status) return SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT; + return SCE_NP_SESSION_SIGNALING_ERROR_CONN_NOT_FOUND; } int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(int32_t context_id, void* info) { diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 806accdb3b..6bffefa394 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -174,6 +174,14 @@ int APS5_VABI sceNpWebApi2PushEventDeleteFilter() { return 0; } +int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int lib_ctx_id, int handle_id, uint32_t timeout) { + (void)handle_id; + (void)timeout; + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(lib_ctx_id)) throw std::invalid_argument("sceNpWebApi2PushEventSetHandleTimeout: unknown context"); + return 0; +} + int APS5_VABI sceNpWebApi2PushEventRegisterPushContextCallback() { return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index cba221154a..db18fe77d6 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -201,7 +201,7 @@ int APS5_VABI scePadResetOrientation(int handle) { int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (enable) NotImplemented_nid_no_patch(__func__); + Pad::SetAngularVelocityDeadband(enable); return PAD_OK; } @@ -314,4 +314,9 @@ int APS5_VABI scePadUnknown_n3kSX62fgNo() { return 0; } +int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 8100e33c58..d38385a704 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -59,6 +59,7 @@ void SetTriggerCommand(int trigger, const std::uint8_t* command); void ResetOrientation(); void SetMotionEnabled(bool enabled); void SetTiltCorrection(bool enabled); +void SetAngularVelocityDeadband(bool enabled); } extern "C" void PadPublishInput_nid_postfix(const PadInputState& input); diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 9d8adf4c31..2ab3f126c3 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -30,6 +30,8 @@ namespace { std::uint64_t lastFuseTime = 0; float biasIntegral[3] = {0.0f, 0.0f, 0.0f}; bool tiltCorrection = true; + bool angularVelocityDeadband = false; + constexpr float kAngularVelocityDeadband = 0.017453292f; std::uint8_t nextTouchId = 0; bool prevTouchActive[2] = {false, false}; std::uint8_t touchIds[2] = {0, 0}; @@ -126,7 +128,12 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin const bool live = state.hasMotion && output.motionEnabled; const std::array rest{0.0f, 9.80665f, 0.0f}; const std::array& accel = live ? state.accel : rest; - const std::array gyro = live ? state.gyro : std::array{0.0f, 0.0f, 0.0f}; + std::array gyro = live ? state.gyro : std::array{0.0f, 0.0f, 0.0f}; + if (angularVelocityDeadband) { + for (float& axis : gyro) { + if (axis < kAngularVelocityDeadband && axis > -kAngularVelocityDeadband) axis = 0.0f; + } + } if (live) { float dt = lastFuseTime != 0 && now > lastFuseTime ? static_cast(now - lastFuseTime) * 1e-6f : 0.0f; dt = std::min(dt, 0.1f); @@ -316,6 +323,11 @@ void Pad::SetTiltCorrection(bool enabled) { biasIntegral[0] = biasIntegral[1] = biasIntegral[2] = 0.0f; } +void Pad::SetAngularVelocityDeadband(bool enabled) { + std::lock_guard lock(stateMutex); + angularVelocityDeadband = enabled; +} + void Pad::SetMotionEnabled(bool enabled) { std::lock_guard lock(stateMutex); if (output.motionEnabled == enabled) return; 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/libScePngDec/Export.cpp b/core/libs/prx/libScePngDec/Export.cpp index cebcc9e3ef..220f2aaeec 100644 --- a/core/libs/prx/libScePngDec/Export.cpp +++ b/core/libs/prx/libScePngDec/Export.cpp @@ -55,6 +55,7 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); std::int32_t validateCreateParam(const PngDecCreateParam* param) { if (!param) return SCE_PNG_DEC_ERROR_INVALID_PARAM; + if (param->this_size != sizeof(PngDecCreateParam)) return SCE_PNG_DEC_ERROR_INVALID_SIZE; if (param->attribute != ATTRIBUTE_NONE && param->attribute != ATTRIBUTE_BIT_DEPTH_16) return SCE_PNG_DEC_ERROR_INVALID_PARAM; if (param->max_image_width == 0 || param->max_image_width - 1 > MAX_IMAGE_WIDTH) return SCE_PNG_DEC_ERROR_INVALID_SIZE; return 0; 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/Filesystem.cpp b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp index b5245f5b1a..a7e6f01754 100644 --- a/core/libs/prx/libScePosixForWebKit/Filesystem.cpp +++ b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp @@ -62,8 +62,8 @@ extern "C" int APS5_VABI mkstemp_nid_postfix(char* pattern) { } extern "C" int APS5_VABI isatty_nid_postfix(int descriptor) { - if (descriptor >= GuestSockets::FirstDescriptor) { - Failure(GuestSocketIsOpen_nid_no_patch(descriptor) ? 25 : 9); + if (GuestSocketIsOpen_nid_no_patch(descriptor)) { + Failure(25); return 0; } #ifdef _WIN32 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/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt new file mode 100644 index 0000000000..4feb3ddf25 --- /dev/null +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceProprietaryVoiceChatHelperPrx SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceProprietaryVoiceChatHelperPrx PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceProprietaryVoiceChatHelperPrx) + +if(BUILD_TESTING) + add_test_executable(guest_proprietary_voice_chat_helper_tests ${LIBS_INCLUDE_DIR}/tests/GuestProprietaryVoiceChatHelper.cpp) + target_include_directories(guest_proprietary_voice_chat_helper_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_proprietary_voice_chat_helper_tests PRIVATE libSceProprietaryVoiceChatHelperPrx libc) + configure_windows_unwind(guest_proprietary_voice_chat_helper_tests) + add_test(NAME guest_proprietary_voice_chat_helper COMMAND guest_proprietary_voice_chat_helper_tests) + if(WIN32) + set_tests_properties(guest_proprietary_voice_chat_helper PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp index 882e6e92c0..315ce70e71 100644 --- a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -1,23 +1,60 @@ +#include +#include +#include + #include "prx/libc/include/General.hpp" +namespace { + +struct Helper { + std::mutex mutex; + bool initialized = false; +}; + +Helper& State() { + static Helper helper; + return helper; +} + +[[noreturn]] void Fail(const char* function, const std::string& why) { + throw std::logic_error(std::string(function) + ": " + why); +} + +void RequireInitialized(const Helper& helper, const char* function) { + if (!helper.initialized) Fail(function, "library not initialized"); +} + +} // namespace + extern "C" { -int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceProprietaryVoiceChatHelperInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { - NotImplemented_nid_no_patch(__func__); + auto& helper = State(); + std::lock_guard lock(helper.mutex); + if (helper.initialized) Fail(__func__, "already initialized"); + helper.initialized = true; return 0; } int APS5_VABI sceProprietaryVoiceChatHelperTerminate(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + helper.initialized = false; + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index e7326689c0..bec0b00b8e 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -155,4 +155,16 @@ int APS5_VABI scePsmlMfsr2Init() { return 0; } +APS5_EXPORT("xNZM9Y5lf-M", scePsmlUnknown01); +int APS5_VABI scePsmlUnknown01(void) { + NotImplemented_nid_no_patch("xNZM9Y5lf-M"); + return 0; +} + +APS5_EXPORT("lcZD9tJraAw", scePsmlUnknown02); +int APS5_VABI scePsmlUnknown02(void) { + NotImplemented_nid_no_patch("lcZD9tJraAw"); + return 0; +} + } diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 7b418022c6..23369d1158 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -59,4 +59,8 @@ int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* descriptio return 0; } +int APS5_VABI sceRazorCpuSync(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index b0f3a56035..0744be6f86 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -286,7 +286,7 @@ bool parseRfc2822DateTime(const char* cursor, RtcDateTime& time, std::int64_t& o ++cursor; if (readDigits(cursor, 2, second) == 0) return false; if (*cursor == ' ' && !parseZone(cursor + 1, offsetMinutes)) return false; - } else if (*cursor != 0 && *cursor != ' ') { + } else if (*cursor != ' ') { return false; } time = makeDateTime(year, month, day, hour, minute, second, 0); diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index bbd31b09ab..2bf31d06e5 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -77,15 +77,7 @@ int APS5_VABI sceRudpGetStatus(void* status, std::size_t size) { return 0; } -int APS5_VABI sceRudpEnd(int ctx_id) { - std::lock_guard lk(g_mutex); - if (!g_inited) { - return RUDP_ERROR_NOT_INITIALIZED; - } - return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; -} - -int APS5_VABI sceRudpTerminate(void) { +int APS5_VABI sceRudpEnd(void) { std::lock_guard lk(g_mutex); if (!g_inited) { return RUDP_ERROR_NOT_INITIALIZED; @@ -95,6 +87,14 @@ int APS5_VABI sceRudpTerminate(void) { return 0; } +int APS5_VABI sceRudpTerminate(int ctx_id) { + std::lock_guard lk(g_mutex); + if (!g_inited) { + return RUDP_ERROR_NOT_INITIALIZED; + } + return g_ctx.erase(ctx_id) != 0 ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; +} + int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priority) { (void)stack_size; (void)priority; diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index b09e7deaa8..08cc546d3d 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -25,6 +25,7 @@ static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; static std::atomic g_initializations{0}; +static std::atomic g_convertProgress{0.0f}; static std::string save_root() { return std::string(SAVE_DIR); @@ -716,6 +717,9 @@ int APS5_VABI sceSaveDataTerminate(void) { return SAVE_DATA_ERROR_BUSY; } --g_initializations; + if (g_initializations == 0) { + g_convertProgress = 0.0f; + } return SAVE_DATA_OK; } @@ -780,6 +784,7 @@ static int convert(const SaveDataConvert* param) { if (param == nullptr || param->dir_name == nullptr) { throw std::runtime_error("sceSaveDataConvert: null argument"); } + g_convertProgress = 0.0f; const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); if (nameEnd == nullptr) { return SAVE_DATA_ERROR_PARAMETER; @@ -791,6 +796,7 @@ static int convert(const SaveDataConvert* param) { if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { return SAVE_DATA_ERROR_NOT_FOUND; } + g_convertProgress = 1.0f; return SAVE_DATA_OK; } @@ -800,9 +806,21 @@ int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { return rc; } -int APS5_VABI sceSaveDataGetConvertProgress() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int getConvertProgress(float* progress) { + if (progress == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (g_initializations == 0) { + return SAVE_DATA_ERROR_NOT_INITIALIZED; + } + *progress = g_convertProgress.load(); + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataGetConvertProgress(float* progress) { + const int rc = getConvertProgress(progress); + SAVEDATA_TRACE("getConvertProgress -> 0x%08x progress=%.2f", static_cast(rc), progress != nullptr && rc == SAVE_DATA_OK ? *progress : 0.0f); + return rc; } int APS5_VABI sceSaveDataCancel() { @@ -810,9 +828,10 @@ int APS5_VABI sceSaveDataCancel() { return 0; } -int APS5_VABI sceSaveDataDirNameSearch2() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, SaveDataDirNameSearchResult* result) { + const int rc = dirNameSearch(cond, result); + SAVEDATA_TRACE("dirNameSearch2 user=%d pattern=%s hit=%u set=%u -> 0x%08x", cond != nullptr ? cond->user_id : -1, cond != nullptr && cond->dir_name != nullptr ? cond->dir_name->data : "(all)", result != nullptr ? result->hit_num : 0u, result != nullptr ? result->set_num : 0u, static_cast(rc)); + return rc; } APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index 0c609afb1d..a313edc364 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -105,9 +105,12 @@ int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int3 } -int APS5_VABI sceShareCaptureVideoClipExtended(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, int32_t* req_id) { + (void)extended_param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareGetRunningStatus(uint32_t* status) { diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 6b0387b949..9f8cc23fee 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -26,4 +26,9 @@ int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA* in return 0; } +int APS5_VABI sceSharePlayProhibitController(int prohibited) { + if (prohibited != 0 && prohibited != 1) APS5_INVALID_ARG_EX; + return 0; +} + } diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 4b3e23a4e9..fb2265f22d 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -134,6 +134,26 @@ int APS5_VABI sceSslFreeSslCertName(void) { return 0; } +int APS5_VABI sceSslDisableVerifyOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetAlpnSelected(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslSetAlpn(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslSetMinSslVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceSslGetIssuerName(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -174,6 +194,11 @@ int APS5_VABI sceSslEnableVerifyOption(void) { 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; diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 67003e4f3d..038095889c 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -147,6 +147,7 @@ inline const std::unordered_map kModuleTable = { {0x00000107, "libSceGic"}, {0x00000108, "libScePlayReady2"}, {0x00000109, "libSceHdrScopes"}, + {0x0000010b, "libSceAcm"}, {0x0000010c, "libSceCesCs"}, {0x0000010d, "libScePlayerInvitationDialog"}, {0x0000010f, "libSceShare"}, @@ -160,6 +161,7 @@ inline const std::unordered_map kModuleTable = { {0x00000120, "libSceAudioPropagation"}, {0x0000012f, "libSceFontGs"}, {0x00000135, "libSceFontGsm"}, + {0x00000137, "libScePsml"}, {0x00000138, "libSceNpPartnerSubscription"}, {0x00000139, "libSceNpAuthAuthorizedAppDialog"}, {0x80000001, "libSceAudioOut"}, diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index b99e85275a..9b90ef5883 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -12,12 +12,15 @@ if(BUILD_TESTING) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) + add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-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) if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit 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 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 fc2b46ac9f..9bdc3e6cac 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,12 +1,21 @@ +#include #include #include +#include #include #include #include +#include #include #include #include #include +#include +#if defined(_WIN32) +#include +#elif defined(__linux__) +#include +#endif #include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -29,18 +38,166 @@ int ConsoleLanguage() { return language; } +constexpr char MainExecutable[] = "/app0/eboot.bin"; +constexpr char ApplicationRoot[] = "/app0/"; +constexpr char MainExecutableVariable[] = "APS5_EBOOT"; +#if defined(_WIN32) +constexpr wchar_t MainExecutableVariableWide[] = L"APS5_EBOOT"; +#endif + +[[noreturn]] void UnconvertedExecutable(const std::string& message) { + NotImplemented_nid_no_patch(("sceSystemServiceLoadExec: " + message).c_str()); + std::abort(); +} + +std::string ConvertedExecutableName(const char* path) { + std::string_view relative(path); + if (!relative.starts_with(ApplicationRoot)) UnconvertedExecutable(std::string("executables outside /app0 (") + path + ")"); + relative.remove_prefix(std::size(ApplicationRoot) - 1); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") UnconvertedExecutable(std::string("a path with an empty, . or .. component (") + path + ")"); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +#if defined(_WIN32) +std::wstring Utf16(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("sceSystemServiceLoadExec: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring QuotedArgument(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} + +struct HostLaunch { + std::wstring executable; + std::wstring commandLine; +}; + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) { + throw std::runtime_error("sceSystemServiceLoadExec: GetModuleFileNameW failed with error " + std::to_string(GetLastError())); + } + own.resize(length); + HostLaunch launch; + const std::filesystem::path ownPath(own); + if (std::strcmp(path, MainExecutable) == 0) { + std::wstring main(32768, L'\0'); + const DWORD mainLength = GetEnvironmentVariableW(MainExecutableVariableWide, main.data(), static_cast(main.size())); + 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()); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.wstring(); + } + if (GetEnvironmentVariableW(MainExecutableVariableWide, nullptr, 0) == 0 && !SetEnvironmentVariableW(MainExecutableVariableWide, own.c_str())) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.commandLine = L"\"" + launch.executable + L"\""; + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) { + launch.commandLine += L" " + QuotedArgument(Utf16(arguments[index])); + } + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(launch.executable.c_str(), launch.commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process)) { + throw std::runtime_error("sceSystemServiceLoadExec: CreateProcessW failed with error " + std::to_string(GetLastError())); + } + CloseHandle(process.hThread); + CloseHandle(process.hProcess); +} +#elif defined(__linux__) +struct HostLaunch { + std::string executable; + std::vector arguments; +}; + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + std::error_code error; + const auto own = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) throw std::runtime_error("sceSystemServiceLoadExec: cannot read /proc/self/exe: " + error.message()); + HostLaunch launch; + if (std::strcmp(path, MainExecutable) == 0) { + const char* main = std::getenv(MainExecutableVariable); + launch.executable = main != nullptr && *main != '\0' ? main : own.string(); + } else { + const auto target = own.parent_path() / (ConvertedExecutableName(path) + own.extension().string()); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.string(); + } + if (setenv(MainExecutableVariable, own.c_str(), 0) != 0) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.arguments.push_back(launch.executable); + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) launch.arguments.emplace_back(arguments[index]); + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { + std::vector argv; + for (auto& argument : launch.arguments) argv.push_back(argument.data()); + argv.push_back(nullptr); + ::execv(launch.executable.c_str(), argv.data()); + throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); +} +#else +struct HostLaunch {}; + +HostLaunch PrepareLaunch(const char*, const char* const*) { + UnconvertedExecutable("starting an executable on this host"); +} + +void StartHostExecutable(HostLaunch&) {} +#endif + } extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - if (std::strcmp(path, "exit") != 0) { - NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement"); - } - (void)arguments; + if (std::strcmp(path, "exit") == 0) LibcExit_nid_no_patch(0); + auto launch = PrepareLaunch(path, arguments); LibcRunShutdown_nid_postfix(); - std::exit(0); + std::fflush(nullptr); + StartHostExecutable(launch); + LibcTerminate_nid_no_patch(0); } int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { @@ -177,8 +334,7 @@ int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { } int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { @@ -191,8 +347,7 @@ int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { } int APS5_VABI sceSystemServiceShowControllerSettings(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceGetAppIdOfRunningBigApp(void) { diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index a0b1d880f7..a9fc644954 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -239,6 +239,7 @@ std::uint64_t APS5_VABI sceUltQueueDataResourcePoolGetWorkAreaSize(std::uint32_t int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { (void)name; + (void)optParam; (void)buildVersion; if (pool == nullptr) { return ULT_ERROR_NULL; @@ -246,7 +247,6 @@ int APS5_VABI sceUltQueueDataResourcePoolCreate(void* pool, const char* name, st if ((reinterpret_cast(pool) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (numData == 0 || numQueueObject == 0 || dataSize == 0 || dataSize > std::numeric_limits::max() / numData) return ULT_ERROR_RANGE; - if (optParam != nullptr) NotImplemented_nid_no_patch("sceUltQueueDataResourcePoolCreate: optParam"); std::lock_guard lock(gMutex); if (waitingQueueResourcePool != nullptr && gResourcePools.find(waitingQueueResourcePool) == gResourcePools.end()) { return ULT_ERROR_INVALID; @@ -283,13 +283,13 @@ int APS5_VABI sceUltQueueDataResourcePoolDestroy(void* pool) { int APS5_VABI sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { (void)name; + (void)optParam; (void)buildVersion; if (queue == nullptr) { return ULT_ERROR_NULL; } if ((reinterpret_cast(queue) & 7u) != 0) return ULT_ERROR_ALIGNMENT; if (dataSize == 0) return ULT_ERROR_RANGE; - if (optParam != nullptr) NotImplemented_nid_no_patch("sceUltQueueCreate: optParam"); auto state = std::make_shared(); state->_dataSize = dataSize; std::lock_guard lock(gMutex); diff --git a/core/libs/prx/libSceUsbStorage/Export.cpp b/core/libs/prx/libSceUsbStorage/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceUsbStorage/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceUsbStorageDialog/Export.cpp b/core/libs/prx/libSceUsbStorageDialog/Export.cpp new file mode 100644 index 0000000000..fb34206c21 --- /dev/null +++ b/core/libs/prx/libSceUsbStorageDialog/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libSceUsbd/CMakeLists.txt b/core/libs/prx/libSceUsbd/CMakeLists.txt index d7acf60da8..0ae07d11b5 100644 --- a/core/libs/prx/libSceUsbd/CMakeLists.txt +++ b/core/libs/prx/libSceUsbd/CMakeLists.txt @@ -1,5 +1,9 @@ add_library(libSceUsbd SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceUsbd PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceUsbd PRIVATE usb-1.0) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceUsbd PRIVATE LINKER:--exclude-libs,ALL) +endif() configure_windows_unwind(libSceUsbd) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index d0be202ca2..9518092966 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -3,25 +3,37 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include + #include +#include #include +#include +#include #include #include #include #include +#include +#include namespace { constexpr std::int32_t SCE_USBD_ERROR_INVALID_ARG = static_cast(0x80240002); +constexpr std::int32_t SCE_USBD_ERROR_NOT_FOUND = static_cast(0x80240005); +constexpr std::int32_t SCE_USBD_ERROR_OTHER = static_cast(0x802400FF); +constexpr std::uint8_t TRANSFER_TYPE_CONTROL = 0; +constexpr std::uint8_t TRANSFER_TYPE_ISOCHRONOUS = 1; +constexpr std::uint8_t TRANSFER_TYPE_BULK = 2; +constexpr std::uint8_t TRANSFER_TYPE_INTERRUPT = 3; +constexpr std::int32_t CONTROL_SETUP_SIZE = 8; +constexpr std::uint8_t SUPPORTED_TRANSFER_FLAGS = LIBUSB_TRANSFER_SHORT_NOT_OK | LIBUSB_TRANSFER_ADD_ZERO_PACKET; struct UsbdTimeval { std::int64_t seconds; std::int64_t microseconds; }; -constexpr std::int32_t SCE_USBD_ERROR_NOT_FOUND = static_cast(0x80240005); -constexpr std::uint8_t TRANSFER_TYPE_INTERRUPT = 3; - struct UsbdIsoPacketDescriptor { std::uint32_t length; std::uint32_t actualLength; @@ -47,15 +59,86 @@ struct UsbdTransfer { }; static_assert(offsetof(UsbdTransfer, timeout) == 12 && offsetof(UsbdTransfer, callback) == 32 && offsetof(UsbdTransfer, numIsoPackets) == 56 && offsetof(UsbdTransfer, isoPacketDescriptors) == 60); +static_assert(sizeof(libusb_device_descriptor) == 18); +static_assert(sizeof(libusb_config_descriptor) == 40 && offsetof(libusb_config_descriptor, interface) == 16 && offsetof(libusb_config_descriptor, extra) == 24 && offsetof(libusb_config_descriptor, extra_length) == 32); +static_assert(sizeof(libusb_interface) == 16 && offsetof(libusb_interface, num_altsetting) == 8); +static_assert(sizeof(libusb_interface_descriptor) == 40 && offsetof(libusb_interface_descriptor, endpoint) == 16 && offsetof(libusb_interface_descriptor, extra) == 24 && offsetof(libusb_interface_descriptor, extra_length) == 32); +static_assert(sizeof(libusb_endpoint_descriptor) == 32 && offsetof(libusb_endpoint_descriptor, wMaxPacketSize) == 4 && offsetof(libusb_endpoint_descriptor, extra) == 16 && offsetof(libusb_endpoint_descriptor, extra_length) == 24); +static_assert(sizeof(libusb_iso_packet_descriptor) == sizeof(UsbdIsoPacketDescriptor)); + +struct UsbdState { + std::mutex mutex; + libusb_context* context = nullptr; + std::uint32_t initCount = 0; + std::unordered_map devices; + std::unordered_set lists; + std::unordered_set handles; + std::unordered_set configs; + std::unordered_map inFlight; +}; + +UsbdState& State() { + static auto* state = new UsbdState(); + return *state; +} void* g_emptyDeviceList[1] = {nullptr}; std::string AbsentDevice(const char* function) { - return std::string(function) + ": the device list is always empty, so the device was not returned by sceUsbdGetDeviceList"; + return std::string(function) + ": the device was not returned by sceUsbdGetDeviceList or was already released"; } std::string UnopenedHandle(const char* function) { - return std::string(function) + ": no device can be opened, so the handle was not returned by sceUsbdOpen"; + return std::string(function) + ": the handle was not returned by sceUsbdOpen or was already closed"; +} + +std::int32_t ToGuestError(int result) { + if (result >= 0) return result; + if (result == LIBUSB_ERROR_OTHER) return SCE_USBD_ERROR_OTHER; + return static_cast(0x80240000u - static_cast(result)); +} + +libusb_device* KnownDevice(void* device, const char* function) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(function)); + return host; +} + +libusb_device_handle* KnownHandle(void* deviceHandle, const char* function) { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(deviceHandle); + if (!state.handles.contains(host)) throw std::runtime_error(UnopenedHandle(function)); + return host; +} + +void AddDeviceReference(UsbdState& state, libusb_device* device) { + ++state.devices[device]; +} + +void DropDeviceReference(UsbdState& state, libusb_device* device) { + const auto found = state.devices.find(device); + if (found == state.devices.end()) throw std::runtime_error(AbsentDevice("sceUsbdUnrefDevice")); + if (--found->second == 0) state.devices.erase(found); +} + +void LIBUSB_CALL CompleteTransfer(libusb_transfer* host) { + auto* guest = static_cast(host->user_data); + guest->status = host->status; + guest->actualLength = host->actual_length; + for (int packet = 0; packet < host->num_iso_packets; ++packet) { + guest->isoPacketDescriptors[packet].actualLength = host->iso_packet_desc[packet].actual_length; + guest->isoPacketDescriptors[packet].status = host->iso_packet_desc[packet].status; + } + { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.inFlight.erase(guest); + } + libusb_free_transfer(host); + if (guest->callback != nullptr) guest->callback(guest); } } @@ -63,27 +146,80 @@ std::string UnopenedHandle(const char* function) { extern "C" { std::int32_t APS5_VABI sceUsbdInit() { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initCount == 0) { + libusb_context* context = nullptr; + if (libusb_init_context(&context, nullptr, 0) == 0) state.context = context; + } + ++state.initCount; return 0; } void APS5_VABI sceUsbdExit() { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initCount == 0 || --state.initCount != 0) return; + if (!state.inFlight.empty()) throw std::runtime_error(std::string(__func__) + ": transfers are still in flight"); + for (auto* handle : state.handles) libusb_close(handle); + for (auto* config : state.configs) libusb_free_config_descriptor(config); + for (auto* list : state.lists) libusb_free_device_list(list, 0); + for (const auto& [device, references] : state.devices) { + for (std::uint32_t reference = 0; reference < references; ++reference) libusb_unref_device(device); + } + state.handles.clear(); + state.configs.clear(); + state.lists.clear(); + state.devices.clear(); + if (state.context != nullptr) libusb_exit(state.context); + state.context = nullptr; } std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list) { if (list == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - *list = g_emptyDeviceList; - return 0; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.context == nullptr) { + *list = g_emptyDeviceList; + return 0; + } + libusb_device** devices = nullptr; + const ssize_t count = libusb_get_device_list(state.context, &devices); + if (count < 0) return ToGuestError(static_cast(count)); + for (ssize_t index = 0; index < count; ++index) AddDeviceReference(state, devices[index]); + state.lists.insert(devices); + *list = reinterpret_cast(devices); + return count; } void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices) { - (void)unrefDevices; - if (list != nullptr && list != g_emptyDeviceList) throw std::runtime_error(std::string(__func__) + ": list was not returned by sceUsbdGetDeviceList"); + if (list == nullptr || list == g_emptyDeviceList) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* devices = reinterpret_cast(list); + if (state.lists.erase(devices) == 0) throw std::runtime_error(std::string(__func__) + ": list was not returned by sceUsbdGetDeviceList"); + if (unrefDevices != 0) { + for (auto** device = devices; *device != nullptr; ++device) DropDeviceReference(state, *device); + } + libusb_free_device_list(devices, unrefDevices); } std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout) { if (timeout == nullptr || timeout->seconds < 0 || timeout->microseconds < 0 || timeout->microseconds >= 1000000) return SCE_USBD_ERROR_INVALID_ARG; - std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); - return 0; + libusb_context* context; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + context = state.context; + } + if (context == nullptr) { + std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); + return 0; + } + timeval host{}; + host.tv_sec = static_cast(timeout->seconds); + host.tv_usec = static_cast(timeout->microseconds); + return ToGuestError(libusb_handle_events_timeout_completed(context, &host, nullptr)); } UsbdTransfer* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets) { @@ -96,6 +232,12 @@ UsbdTransfer* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets) { } void APS5_VABI sceUsbdFreeTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr) return; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.inFlight.contains(transfer)) throw std::runtime_error(std::string(__func__) + ": transfer is still in flight"); + } std::free(transfer); } @@ -111,9 +253,99 @@ void APS5_VABI sceUsbdFillInterruptTransfer(UsbdTransfer* transfer, void* device transfer->callback = callback; } +void APS5_VABI sceUsbdFillBulkTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = endpoint; + transfer->type = TRANSFER_TYPE_BULK; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->userData = userData; + transfer->callback = callback; +} + +void APS5_VABI sceUsbdFillIsoTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, std::int32_t numIsoPackets, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = endpoint; + transfer->type = TRANSFER_TYPE_ISOCHRONOUS; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = length; + transfer->numIsoPackets = numIsoPackets; + transfer->userData = userData; + transfer->callback = callback; +} + +void APS5_VABI sceUsbdFillControlSetup(std::uint8_t* buffer, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint16_t length) { + if (buffer == nullptr) throw std::runtime_error(std::string(__func__) + ": null buffer"); + buffer[0] = requestType; + buffer[1] = request; + buffer[2] = static_cast(value); + buffer[3] = static_cast(value >> 8); + buffer[4] = static_cast(index); + buffer[5] = static_cast(index >> 8); + buffer[6] = static_cast(length); + buffer[7] = static_cast(length >> 8); +} + +void APS5_VABI sceUsbdFillControlTransfer(UsbdTransfer* transfer, void* deviceHandle, std::uint8_t* buffer, UsbdTransferCallback callback, void* userData, std::uint32_t timeout) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + transfer->deviceHandle = deviceHandle; + transfer->endpoint = 0; + transfer->type = TRANSFER_TYPE_CONTROL; + transfer->timeout = timeout; + transfer->buffer = buffer; + transfer->length = buffer != nullptr ? CONTROL_SETUP_SIZE + (buffer[6] | buffer[7] << 8) : 0; + transfer->userData = userData; + transfer->callback = callback; +} + +std::uint8_t* APS5_VABI sceUsbdControlTransferGetData(UsbdTransfer* transfer) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + return transfer->buffer != nullptr ? transfer->buffer + CONTROL_SETUP_SIZE : nullptr; +} + +std::uint8_t* APS5_VABI sceUsbdControlTransferGetSetup(UsbdTransfer* transfer) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + return transfer->buffer; +} + +void APS5_VABI sceUsbdSetIsoPacketLengths(UsbdTransfer* transfer, std::uint32_t length) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + for (std::int32_t packet = 0; packet < transfer->numIsoPackets; ++packet) transfer->isoPacketDescriptors[packet].length = length; +} + +std::uint8_t* APS5_VABI sceUsbdGetIsoPacketBuffer(UsbdTransfer* transfer, std::uint32_t packet) { + if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); + if (transfer->numIsoPackets < 0 || packet >= static_cast(transfer->numIsoPackets) || transfer->buffer == nullptr) return nullptr; + std::uint8_t* pointer = transfer->buffer; + for (std::uint32_t index = 0; index < packet; ++index) pointer += transfer->isoPacketDescriptors[index].length; + return pointer; +} + +std::int32_t APS5_VABI sceUsbdBulkTransfer(void* deviceHandle, std::uint8_t endpoint, std::uint8_t* data, std::int32_t length, std::int32_t* transferred, std::uint32_t timeout) { + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + if (transferred == nullptr) throw std::runtime_error(std::string(__func__) + ": null transferred output"); + *transferred = 0; + return ToGuestError(libusb_bulk_transfer(KnownHandle(deviceHandle, __func__), endpoint, data, length, transferred, timeout)); +} + +std::int32_t APS5_VABI sceUsbdInterruptTransfer(void* deviceHandle, std::uint8_t endpoint, std::uint8_t* data, std::int32_t length, std::int32_t* transferred, std::uint32_t timeout) { + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + if (transferred == nullptr) throw std::runtime_error(std::string(__func__) + ": null transferred output"); + *transferred = 0; + return ToGuestError(libusb_interrupt_transfer(KnownHandle(deviceHandle, __func__), endpoint, data, length, transferred, timeout)); +} + std::int32_t APS5_VABI sceUsbdCancelTransfer(UsbdTransfer* transfer) { if (transfer == nullptr) throw std::runtime_error(std::string(__func__) + ": null transfer"); - return SCE_USBD_ERROR_NOT_FOUND; + auto& state = State(); + std::lock_guard lock(state.mutex); + const auto found = state.inFlight.find(transfer); + if (found == state.inFlight.end()) return SCE_USBD_ERROR_NOT_FOUND; + return ToGuestError(libusb_cancel_transfer(found->second)); } std::int32_t APS5_VABI sceUsbdEventHandlingOk() { @@ -121,115 +353,185 @@ std::int32_t APS5_VABI sceUsbdEventHandlingOk() { } void APS5_VABI sceUsbdFreeConfigDescriptor(void* config) { - if (config != nullptr) throw std::runtime_error(std::string(__func__) + ": descriptor was not returned by this library"); + if (config == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(config); + if (state.configs.erase(host) == 0) throw std::runtime_error(std::string(__func__) + ": descriptor was not returned by this library"); + libusb_free_config_descriptor(host); } std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber) { (void)interfaceNumber; if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + KnownHandle(deviceHandle, __func__); + return 0; } std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle) { if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + int configuration = 0; + const int result = libusb_get_configuration(KnownHandle(deviceHandle, __func__), &configuration); + return result == LIBUSB_ERROR_NO_DEVICE ? ToGuestError(LIBUSB_ERROR_NO_DEVICE) : 0; } std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber) { - (void)interfaceNumber; if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_claim_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); } void APS5_VABI sceUsbdClose(void* deviceHandle) { - if (deviceHandle != nullptr) throw std::runtime_error(UnopenedHandle(__func__)); + if (deviceHandle == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(deviceHandle); + if (state.handles.erase(host) == 0) throw std::runtime_error(UnopenedHandle(__func__)); + libusb_close(host); } std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout) { - (void)requestType; - (void)request; - (void)value; - (void)index; - (void)timeout; if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_control_transfer(KnownHandle(deviceHandle, __func__), requestType, request, value, index, data, length, timeout)); } std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config) { - (void)config; if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); - throw std::runtime_error(AbsentDevice(__func__)); + if (config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + libusb_config_descriptor* host = nullptr; + const int result = libusb_get_active_config_descriptor(KnownDevice(device, __func__), &host); + *config = nullptr; + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.configs.insert(host); + *config = host; + return 0; } std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device) { if (device == nullptr) return 0; - throw std::runtime_error(AbsentDevice(__func__)); + return libusb_get_bus_number(KnownDevice(device, __func__)); } std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config) { - (void)configIndex; if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(AbsentDevice(__func__)); + libusb_config_descriptor* host = nullptr; + const int result = libusb_get_config_descriptor(KnownDevice(device, __func__), configIndex, &host); + *config = nullptr; + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.configs.insert(host); + *config = host; + return 0; } std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device) { if (device == nullptr) return 0; - throw std::runtime_error(AbsentDevice(__func__)); + return libusb_get_device_address(KnownDevice(device, __func__)); } std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor) { if (device == nullptr || descriptor == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(AbsentDevice(__func__)); + return ToGuestError(libusb_get_device_descriptor(KnownDevice(device, __func__), static_cast(descriptor))); } std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length) { - (void)descriptorIndex; - (void)languageId; if (deviceHandle == nullptr || data == nullptr || length < 1) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + auto* handle = KnownHandle(deviceHandle, __func__); + if (length > 65535) length = 65535; + data[0] = 0; + return ToGuestError(libusb_get_string_descriptor(handle, descriptorIndex, languageId, data, length)); } std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber) { - (void)interfaceNumber; if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_kernel_driver_active(KnownHandle(deviceHandle, __func__), interfaceNumber)); } std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle) { if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(AbsentDevice(__func__)); + *deviceHandle = nullptr; + libusb_device_handle* host = nullptr; + const int result = libusb_open(KnownDevice(device, __func__), &host); + if (result != 0) return ToGuestError(result); + auto& state = State(); + std::lock_guard lock(state.mutex); + state.handles.insert(host); + *deviceHandle = host; + return 0; } void* APS5_VABI sceUsbdRefDevice(void* device) { if (device == nullptr) return nullptr; - throw std::runtime_error(AbsentDevice(__func__)); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + if (!state.devices.contains(host)) throw std::runtime_error(AbsentDevice(__func__)); + AddDeviceReference(state, host); + return libusb_ref_device(host); } std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber) { - (void)interfaceNumber; if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_release_interface(KnownHandle(deviceHandle, __func__), interfaceNumber)); } std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle) { if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_reset_device(KnownHandle(deviceHandle, __func__))); } std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration) { - (void)configuration; if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + return ToGuestError(libusb_set_configuration(KnownHandle(deviceHandle, __func__), configuration)); } std::int32_t APS5_VABI sceUsbdSubmitTransfer(UsbdTransfer* transfer) { if (transfer == nullptr || transfer->deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; - throw std::runtime_error(UnopenedHandle(__func__)); + auto* handle = KnownHandle(transfer->deviceHandle, __func__); + if ((transfer->flags & ~SUPPORTED_TRANSFER_FLAGS) != 0) throw std::runtime_error(std::string(__func__) + ": transfer flags " + std::to_string(transfer->flags) + " are not supported"); + if (transfer->numIsoPackets < 0) return SCE_USBD_ERROR_INVALID_ARG; + auto* host = libusb_alloc_transfer(transfer->numIsoPackets); + if (host == nullptr) return ToGuestError(LIBUSB_ERROR_NO_MEM); + host->dev_handle = handle; + host->flags = transfer->flags; + host->endpoint = transfer->endpoint; + host->type = transfer->type; + host->timeout = transfer->timeout; + host->length = transfer->length; + host->buffer = transfer->buffer; + host->user_data = transfer; + host->callback = CompleteTransfer; + host->num_iso_packets = transfer->numIsoPackets; + for (int packet = 0; packet < transfer->numIsoPackets; ++packet) host->iso_packet_desc[packet].length = transfer->isoPacketDescriptors[packet].length; + auto& state = State(); + { + std::lock_guard lock(state.mutex); + if (!state.inFlight.emplace(transfer, host).second) { + libusb_free_transfer(host); + return ToGuestError(LIBUSB_ERROR_BUSY); + } + } + const int result = libusb_submit_transfer(host); + if (result != 0) { + { + std::lock_guard lock(state.mutex); + state.inFlight.erase(transfer); + } + libusb_free_transfer(host); + } + return ToGuestError(result); } void APS5_VABI sceUsbdUnrefDevice(void* device) { - if (device != nullptr) throw std::runtime_error(AbsentDevice(__func__)); + if (device == nullptr) return; + auto& state = State(); + std::lock_guard lock(state.mutex); + auto* host = static_cast(device); + DropDeviceReference(state, host); + libusb_unref_device(host); } } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index faef0b4583..6b9a38c833 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -27,12 +27,47 @@ configure_windows_unwind(libSceVideoOut) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() +add_test_executable(video_out_mouse_tests EXCLUDE_FROM_ALL tests/MouseInput.cpp) +target_include_directories(video_out_mouse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) +target_link_libraries(video_out_mouse_tests PRIVATE libSceVideoOut libSceMouse libc SDL2-static) +set_target_properties(video_out_mouse_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(video_out_mouse_tests) +if(BUILD_TESTING) + set_target_properties(video_out_mouse_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME video_out_mouse COMMAND video_out_mouse_tests) + if(WIN32) + set_tests_properties(video_out_mouse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$;PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(video_out_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp) target_include_directories(video_out_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) + add_test_executable(video_out_output_mode_policy_tests tests/OutputModePolicy.cpp) + target_include_directories(video_out_output_mode_policy_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_output_mode_policy_tests PRIVATE libSceVideoOut libc libkernel) + configure_windows_unwind(video_out_output_mode_policy_tests) + add_test(NAME video_out_output_mode_policy COMMAND video_out_output_mode_policy_tests) + set_tests_properties(video_out_output_mode_policy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(video_out_output_mode_policy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_video_out_high_frame_rate_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHighFrameRate.cpp) + target_include_directories(guest_video_out_high_frame_rate_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_high_frame_rate_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_high_frame_rate_tests) + add_test(NAME guest_video_out_high_frame_rate COMMAND guest_video_out_high_frame_rate_tests) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ddeb807c60..0c946149c5 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,6 +7,16 @@ extern "C" { +int APS5_VABI sceVideoOutSetFlipMaster() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutClearFlipMaster() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceVideoOut/include/Event.hpp b/core/libs/prx/libSceVideoOut/include/Event.hpp index 46dade9d34..ca5e4f09f7 100644 --- a/core/libs/prx/libSceVideoOut/include/Event.hpp +++ b/core/libs/prx/libSceVideoOut/include/Event.hpp @@ -18,6 +18,7 @@ int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutDeleteOutputModeEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle); +int APS5_VABI sceVideoOutDeleteVrrActiveStatusEvent(KernelEqueue eq, int handle); int APS5_VABI sceVideoOutGetEventCount(const KernelEvent* ev); int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data); diff --git a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp index 56ea31b6d7..be37a2c2cb 100644 --- a/core/libs/prx/libSceVideoOut/include/MouseInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/MouseInput.hpp @@ -2,13 +2,24 @@ #define CORE_LIBS_PRX_LIBSCEVIDEOOUT_MOUSEINPUT_HPP #include "SDL_events.h" +#include class MouseInput { public: void HandleEvent(const SDL_Event& event, unsigned windowId); + void SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight); + bool CaptureWanted(bool allowed) const; + void UpdateCapture(bool allowed); private: + std::int32_t scaled(std::int32_t delta, double scale, double& remainder); + bool focused = true; + bool captured = false; + double scaleX = 1.0; + double scaleY = 1.0; + double remainderX = 0.0; + double remainderY = 0.0; }; #endif diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index a188ee6007..4ecf913417 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -5,6 +5,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate); + extern "C" { int APS5_VABI sceVideoOutOpen(int user_id, int bus_type, int index, const void* param); diff --git a/core/libs/prx/libSceVideoOut/include/PadInput.hpp b/core/libs/prx/libSceVideoOut/include/PadInput.hpp index ce51888771..b68b5e5cd3 100644 --- a/core/libs/prx/libSceVideoOut/include/PadInput.hpp +++ b/core/libs/prx/libSceVideoOut/include/PadInput.hpp @@ -17,6 +17,7 @@ public: ~PadInput(); void HandleEvent(const SDL_Event& event, DisplayWindow& window); void Update(); + bool MouseModeEnabled() const { return mouseEnabled; } private: explicit PadInput(Pad::InputConfiguration configuration); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 11fa5eb89f..8cfcc7299c 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -215,6 +215,8 @@ public: std::shared_ptr GetConfig(int handle); bool IsOpen(int handle); bool HasConfig(int handle); + bool SupportsHighFrameRate(); + int GetDisplayRefreshRate(); // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); @@ -223,7 +225,8 @@ private: bool close(int handle); void presentLoop(std::stop_token token, std::promise& started); void vblankLoop(std::stop_token token); - void vblankEnd(); + void vblankEnd(std::uint64_t tick); + void updateDisplayRefreshRate(); void processFlip(FlipRequest& req); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); @@ -235,6 +238,7 @@ private: std::shared_ptr flipQueue = std::make_shared(); DisplayWindow window; + std::atomic displayRefreshRate{0}; std::jthread presentThread; std::jthread vblankThread; diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index a2a425f246..d13e6494fb 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -163,6 +163,12 @@ int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutDeleteVrrActiveStatusEvent(KernelEqueue eq, int handle) try { + return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VRR_STATUS); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) { if (ev == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 3a5443e6e8..c00aef3ff3 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -2,14 +2,17 @@ #include "prx/libSceMouse/include/MouseState.hpp" #include "SDL_mouse.h" +#include "SDL_video.h" +#include +#include void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { MouseInputEvent input{}; switch (event.type) { case SDL_MOUSEMOTION: if (event.motion.windowID != windowId || !focused) return; - input.x = event.motion.xrel; - input.y = event.motion.yrel; + input.x = scaled(event.motion.xrel, scaleX, remainderX); + input.y = scaled(event.motion.yrel, scaleY, remainderY); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: @@ -53,3 +56,46 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { } MousePublishInput_nid_postfix(input); } + +void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { + if (sourceWidth == 0 || sourceHeight == 0 || presentedWidth == 0 || presentedHeight == 0) return; + const double x = static_cast(sourceWidth) / static_cast(presentedWidth); + const double y = static_cast(sourceHeight) / static_cast(presentedHeight); + if (x != scaleX || y != scaleY) { + std::fprintf(stderr, "[mouse] video driver %s: guest %ux%u presented at %ux%u, delta scale %.4f x %.4f\n", SDL_GetCurrentVideoDriver() ? SDL_GetCurrentVideoDriver() : "?", sourceWidth, sourceHeight, presentedWidth, presentedHeight, x, y); + scaleX = x; + scaleY = y; + remainderX = 0.0; + remainderY = 0.0; + } +} + +std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remainder) { + const double exact = static_cast(delta) * scale + remainder; + const double whole = std::trunc(exact); + remainder = exact - whole; + return static_cast(whole); +} + +bool MouseInput::CaptureWanted(bool allowed) const { + return allowed && focused && MouseIsOpen_nid_postfix(); +} + +void MouseInput::UpdateCapture(bool allowed) { + const bool wanted = CaptureWanted(allowed); + if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; + if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { + static bool warned = false; + if (!warned) { + warned = true; + std::fprintf(stderr, "[mouse] relative mouse mode unavailable: %s\n", SDL_GetError()); + } + return; + } + int ignoredX = 0; + int ignoredY = 0; + SDL_GetRelativeMouseState(&ignoredX, &ignoredY); + remainderX = 0.0; + remainderY = 0.0; + captured = wanted; +} diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 067e01908a..f47aac9f3e 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -11,17 +11,19 @@ #include "prx/libSceVideoOut/include/Output.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" 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 int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { - if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; - if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { switch (mode) { case VIDEO_OUT_OUTPUT_MODE_DEFAULT: break; - case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case VIDEO_OUT_OUTPUT_MODE_119_88HZ: case 0x10: case 0x11: case 0x13: + case VIDEO_OUT_OUTPUT_MODE_119_88HZ: + if (displayRefreshRate < 120) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; + break; + case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case 0x10: case 0x11: case 0x13: return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; case 0x12: throw std::runtime_error(std::string(__func__) + ": output mode 0x12 answers differently on two consoles"); @@ -37,6 +39,12 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO return 0; } +static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; + return ValidateVideoOutOutputMode_nid_no_patch(mode, options, VideoOutDriver::Get().GetDisplayRefreshRate()); +} + static void validateOpenParam(const void* param) { if (param == nullptr) return; VideoOutOpenParam openParam{}; @@ -235,6 +243,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO if (supported < 0) { return supported; } + if (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && (GetAppAttribute3_nid_no_patch() & 0x40u) == 0) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index fbe9da37b0..5b3dcdc23b 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -12,6 +12,7 @@ #include "SDL_vulkan.h" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceVideoOut/include/KeyboardInput.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -447,12 +448,29 @@ void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* tri } } -void VideoOutDriver::vblankEnd() { +bool VideoOutDriver::SupportsHighFrameRate() { + return GetDisplayRefreshRate() >= 120; +} + +int VideoOutDriver::GetDisplayRefreshRate() { + return displayRefreshRate.load(); +} + +void VideoOutDriver::updateDisplayRefreshRate() { + const int display = SDL_GetWindowDisplayIndex(window.Handle()); + if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); + SDL_DisplayMode mode{}; + if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); + displayRefreshRate.store(mode.refresh_rate); +} + +void VideoOutDriver::vblankEnd(std::uint64_t tick) { std::lock_guard lock(mutex); for (const auto& cfg : contexts) { if (!cfg) continue; std::lock_guard cfgLock(cfg->mutex); if (!cfg->opened || cfg->failure) continue; + if (cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ && tick % 2 != 0) continue; require(cfg->vblankStatus.count != std::numeric_limits::max(), "vblank counter overflow"); ++cfg->preVblankStatus.count; cfg->preVblankStatus.processTime = sceKernelGetProcessTime(); @@ -477,6 +495,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.ReleaseLocked(); return; } + require(req.cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ || SupportsHighFrameRate(), "display no longer supports 119.88 Hz output"); require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request"); const auto interval = static_cast(req.flipRate + 1); require(req.cfg->lastFlipVblank <= std::numeric_limits::max() - interval, "flip interval overflow"); @@ -555,6 +574,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { try { window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); + updateDisplayRefreshRate(); const auto extensions = windowExtensions(window.Handle()); AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr}); } catch (...) { @@ -599,11 +619,20 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } } padInput.Update(); + updateDisplayRefreshRate(); + mouseInput.UpdateCapture(!padInput.MouseModeEnabled()); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); current->timing->Add(current->timing->Get("VideoOut", "queue"), dequeued - current->queuedAt); processFlip(*current); + int windowWidth = 0; + int windowHeight = 0; + if (window.Handle() != nullptr) SDL_GetWindowSize(window.Handle(), &windowWidth, &windowHeight); + if (windowWidth > 0 && windowHeight > 0) { + const auto presented = AgcDriver::ComputeContainSize_nid_postfix(current->width, current->height, static_cast(windowWidth), static_cast(windowHeight), true); + mouseInput.SetPresentation(current->width, current->height, presented.width, presented.height); + } const auto finished = AgcDriver::FrameTiming::Clock::now(); AgcDriver::FrameTiming::Clock::duration interval{}; { @@ -658,7 +687,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } void VideoOutDriver::vblankLoop(std::stop_token token) { - using Frame = std::chrono::duration>; + using Frame = std::chrono::duration>; const auto start = std::chrono::steady_clock::now(); try { for (int64_t frame = 1; !token.stop_requested(); ++frame) { @@ -669,7 +698,7 @@ void VideoOutDriver::vblankLoop(std::stop_token token) { std::lock_guard lock(flipQueue->mutex); if (token.stop_requested() || flipQueue->failure) return; } - vblankEnd(); + vblankEnd(static_cast(frame)); } } catch (const std::exception& error) { std::fprintf(stderr, "[videoout] vblank failed: %s\n", error.what()); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 2be38015fb..b90bbc768a 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -306,7 +306,8 @@ void testControls() { check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); - check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode is supported"); + const int highFrameRate = sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0); + check(highFrameRate == 1 || highFrameRate == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unexpected high-frame-rate support result"); check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode was configured"); check(sceVideoOutIsOutputSupported(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode is supported"); check(sceVideoOutConfigureOutput(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode was configured"); @@ -368,6 +369,14 @@ void testControls() { check(sceVideoOutGetEventId(&vrrStatus) == VIDEO_OUT_EVENT_VRR_STATUS, "VRR status event id rejected"); check(sceVideoOutVrrPegToFixedRate(handle, 0, 0) == 0, "VRR peg on an open handle failed"); check(sceVideoOutVrrUnpegFromFixedRate(handle) == 0, "VRR unpeg on an open handle failed"); + check(sceVideoOutDeleteVrrActiveStatusEvent(queue, handle) == 0, "VRR status unsubscription failed"); + { + std::lock_guard lock(cfg->mutex); + check(cfg->vrrStatusEvents.empty(), "VRR status unsubscription did not clear registration"); + } + check(sceVideoOutDeleteVrrActiveStatusEvent(queue, handle) == 0, "duplicate VRR status unsubscription failed"); + expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(0, handle); }); + expectFailure([&] { sceVideoOutDeleteVrrActiveStatusEvent(queue, -1); }); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 1, 0); }); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0, 1); }); expectFailure([&] { sceVideoOutVrrPegToFixedRate(handle, 0x100000000ull, 0); }); diff --git a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp new file mode 100644 index 0000000000..5d740a05e4 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceVideoOut/include/MouseInput.hpp" +#include "prx/libSceMouse/include/mouse_structs.h" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SDL.h" +#include "SDL_events.h" +#include "SDL_mouse.h" +#include +#include +#include +#undef main + +extern "C" { +int APS5_VABI sceMouseInit(); +int APS5_VABI sceMouseOpen(int userId, std::int32_t type, std::int32_t index, const void* param); +int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t num); +int APS5_VABI sceMouseClose(std::int32_t handle); +} + +namespace { + +void Require(bool condition, const char* reason) { + if (!condition) { + std::fprintf(stderr, "mouse input: %s\n", reason); + std::abort(); + } +} + +SDL_Event Motion(unsigned windowId, int xrel, int yrel) { + SDL_Event event{}; + event.type = SDL_MOUSEMOTION; + event.motion.windowID = windowId; + event.motion.xrel = xrel; + event.motion.yrel = yrel; + return event; +} + +SDL_Event Window(unsigned windowId, std::uint8_t kind) { + SDL_Event event{}; + event.type = SDL_WINDOWEVENT; + event.window.windowID = windowId; + event.window.event = kind; + return event; +} +void CaptureDecision(MouseInput& input, std::int32_t handle) { + Require(input.CaptureWanted(true), "a focused window with an open mouse does not want the capture"); + Require(!input.CaptureWanted(false), "the pad's mouse-to-stick toggle does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(!input.CaptureWanted(true), "losing focus does not release the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); + Require(input.CaptureWanted(true), "regaining focus does not restore the capture"); + input.HandleEvent(Window(4, SDL_WINDOWEVENT_FOCUS_LOST), 3); + Require(input.CaptureWanted(true), "another window's focus loss released the capture"); + Require(sceMouseClose(handle) == 0, "close failed"); + Require(!input.CaptureWanted(true), "a closed mouse keeps the capture"); + Require(sceMouseOpen(255, 0, 0, nullptr) >= 0, "reopen failed"); + Require(input.CaptureWanted(true), "a reopened mouse does not want the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_CLOSE), 3); + Require(!input.CaptureWanted(true), "a closed window keeps the capture"); + input.HandleEvent(Window(3, SDL_WINDOWEVENT_FOCUS_GAINED), 3); +} +void CaptureApplied(MouseInput& input) { + SDL_SetHint("SDL_VIDEODRIVER", "dummy"); + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + std::fprintf(stderr, "mouse input: SDL video unavailable (%s): capture application not checked\n", SDL_GetError()); + return; + } + input.UpdateCapture(true); + const bool applied = SDL_GetRelativeMouseMode() == SDL_TRUE; + input.UpdateCapture(false); + Require(SDL_GetRelativeMouseMode() == SDL_FALSE, "the capture was not released"); + std::printf("mouse input: SDL %s relative mode under the %s driver\n", applied ? "applied" : "refused", SDL_GetCurrentVideoDriver()); + SDL_Quit(); +} +void Sum(std::int32_t handle, std::int32_t& x, std::int32_t& y) { + MouseData data[MOUSE_MAX_DATA_NUM]{}; + const int count = sceMouseRead(handle, data, MOUSE_MAX_DATA_NUM); + Require(count >= 0, "read failed"); + x = 0; + y = 0; + for (int index = 0; index < count; ++index) { + x += data[index].x_axis; + y += data[index].y_axis; + } +} + +} + +int main() { + Require(sceMouseInit() == 0, "init failed"); + const std::int32_t handle = sceMouseOpen(255, 0, 0, nullptr); + Require(handle >= 0, "open failed"); + MouseInput input; + std::int32_t x = 0; + std::int32_t y = 0; + + input.HandleEvent(Motion(3, 10, -7), 3); + input.HandleEvent(Motion(4, 100, 100), 3); + Sum(handle, x, y); + Require(x == 10 && y == -7, "unscaled deltas are not passed through or another window's motion was not ignored"); + + input.SetPresentation(1920, 1080, 1843, 1037); + for (int step = 0; step < 5; ++step) input.HandleEvent(Motion(3, 10, -7), 3); + Sum(handle, x, y); + Require(x == 52 && y == -36, "deltas are not scaled by the presented image with sub-pixel carry"); + + input.SetPresentation(1920, 1080, 960, 540); + input.HandleEvent(Motion(3, 1, 1), 3); + input.HandleEvent(Motion(3, 1, 1), 3); + Sum(handle, x, y); + Require(x == 4 && y == 4, "a half-size presentation does not double the deltas"); + + input.SetPresentation(0, 0, 0, 0); + input.HandleEvent(Motion(3, 3, 3), 3); + Sum(handle, x, y); + Require(x == 6 && y == 6, "an empty presentation changed the scale"); + CaptureDecision(input, handle); + CaptureApplied(input); + std::puts("mouse input tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp new file mode 100644 index 0000000000..5a4a600944 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceVideoOut/include/Output.hpp" +#include +#include +#include + +static void Require(bool value, const char* reason) { + if (!value) { + std::fprintf(stderr, "%s\n", reason); + std::abort(); + } +} + +int main() { + constexpr int unsupported = static_cast(0x80290016); + constexpr int invalidValue = static_cast(0x80290001); + constexpr int unknownMode = static_cast(0x8029001e); + VideoOutOutputOptions options{}; + Require(sceVideoOutInitializeOutputOptions(&options) == 0, "output options initialization failed"); + for (int refresh : {-1, 0, 30, 59, 60, 119, 120, 144, 240}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &options, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, nullptr, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 without options did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(1, &options, refresh) == 0, "default mode depended on the display refresh rate"); + } + for (std::uint64_t mode : {4, 7, 8, 12, 13, 14, 16, 17, 19}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(mode, &options, 240) == unsupported, "unimplemented output mode was accepted on a fast display"); + } + Require(ValidateVideoOutOutputMode_nid_no_patch(0xd000000a, &options, 240) == unknownMode, "unknown output mode was accepted"); + VideoOutOutputOptions changed = options; + changed.internalData[0] = 0; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == invalidValue, "mode 15 accepted invalid output options"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 60) == unsupported, "invalid options changed unsupported-mode error precedence"); + changed = options; + changed.internalData[3] = 0xffffffff; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == 0, "mode 15 rejected the free options word"); + bool thrown = false; + try { + ValidateVideoOutOutputMode_nid_no_patch(18, &options, 120); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown, "unsettled mode 18 did not throw"); + +} diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index dd35e045a0..45939e99e8 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -28,6 +28,8 @@ int APS5_VABI sceWebBrowserDialogInitialize(void) { int APS5_VABI sceWebBrowserDialogTerminate(void) { int expected = 1; + if (g_status.compare_exchange_strong(expected, 0)) return 0; + expected = COMMON_DIALOG_STATUS_FINISHED; if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); return 0; } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ce564fc871..50a8682684 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp @@ -47,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 ) @@ -69,6 +71,15 @@ if(BUILD_TESTING) set_tests_properties(guest_array_callbacks PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_dynamic_cast_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicCast.cpp) + target_include_directories(guest_dynamic_cast_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_dynamic_cast_tests PRIVATE libc) + configure_windows_unwind(guest_dynamic_cast_tests) + add_test(NAME guest_dynamic_cast COMMAND guest_dynamic_cast_tests) + if(WIN32) + set_tests_properties(guest_dynamic_cast PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_time_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestTimeConversion.cpp) target_include_directories(guest_time_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_time_conversion_tests PRIVATE libc) @@ -204,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) @@ -345,6 +356,16 @@ if(BUILD_TESTING) set_tests_properties(guest_qsort PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_strftime_tests ${LIBS_INCLUDE_DIR}/tests/GuestStrftime.cpp) + target_include_directories(guest_strftime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_strftime_tests PRIVATE libc) + configure_windows_unwind(guest_strftime_tests) + add_test(NAME guest_strftime COMMAND guest_strftime_tests) + set_tests_properties(guest_strftime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_strftime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_atoi_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtoi.cpp) target_include_directories(guest_atoi_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_atoi_tests PRIVATE libc) @@ -392,6 +413,18 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) + target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_call_trace_tests PRIVATE libc) + configure_windows_unwind(guest_call_trace_tests) + add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) + add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_syslog_tests PRIVATE libc) @@ -423,6 +456,16 @@ if(BUILD_TESTING) target_include_directories(guest_new_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_new_handler_tests PRIVATE libc) configure_windows_unwind(guest_new_handler_tests) + add_test_executable(guest_generic_category_tests ${LIBS_INCLUDE_DIR}/tests/GuestGenericCategory.cpp) + target_include_directories(guest_generic_category_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_generic_category_tests PRIVATE libc) + configure_windows_unwind(guest_generic_category_tests) + add_test(NAME guest_generic_category COMMAND guest_generic_category_tests) + set_tests_properties(guest_generic_category PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_generic_category PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME guest_new_handler COMMAND guest_new_handler_tests) if(WIN32) set_tests_properties(guest_new_handler PROPERTIES ENVIRONMENT_MODIFICATION @@ -567,6 +610,16 @@ if(BUILD_TESTING) set_tests_properties(log_macros PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) + target_include_directories(guest_heap_report_for_game_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_heap_report_for_game_tests PRIVATE libc) + configure_windows_unwind(guest_heap_report_for_game_tests) + add_test(NAME guest_heap_report_for_game COMMAND guest_heap_report_for_game_tests) + if(WIN32) + set_tests_properties(guest_heap_report_for_game PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) @@ -608,6 +661,30 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_mutex_init_with_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcMutexInitWithName.cpp) +target_include_directories(guest_libc_mutex_init_with_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_mutex_init_with_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_mutex_init_with_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_mutex_init_with_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_mutex_init_with_name COMMAND guest_libc_mutex_init_with_name_tests) + if(WIN32) + set_tests_properties(guest_libc_mutex_init_with_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) +target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_timed_wait_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_timed_wait_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_timed_wait COMMAND guest_libc_cnd_timed_wait_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_timed_wait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) if(BUILD_TESTING) enable_language(ASM) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index cf23ee77bd..6056428c98 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -26,12 +26,6 @@ int APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix() { } -APS5_EXPORT("H+8UBOwfScI", libcCyberUnknown08); -std::uint64_t APS5_VABI libcCyberUnknown08(void) { - NotImplemented_nid_no_patch("H+8UBOwfScI"); - return 0; -} - APS5_EXPORT("H2e8t5ScQGc", libc___cxa_finalize); void APS5_VABI libc___cxa_finalize(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); diff --git a/core/libs/prx/libc/include/CallTrace.hpp b/core/libs/prx/libc/include/CallTrace.hpp new file mode 100644 index 0000000000..2c1f46c600 --- /dev/null +++ b/core/libs/prx/libc/include/CallTrace.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CALLTRACE_HPP + +#include +#include + +struct CallTraceEntry { + std::uint64_t sequence; + std::uint64_t thread; + std::uint64_t returnAddress; + const char* name; + const char* library; +}; + +struct CallTraceCount { + std::uint64_t calls; + const char* name; + const char* library; +}; + +extern "C" { +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library); +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity); +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total); +} + +#endif diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 88cee16475..4ad9fa00b8 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -13,6 +13,8 @@ #ifdef _WIN32 #include #include "prx/libc/include/FileLock.hpp" +extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); +extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif struct GuestFilePrefix { @@ -36,6 +38,10 @@ static constexpr const char* FOPEN_MSG_NULL_ARG = "null argument"; static constexpr const char* FOPEN_MSG_NOT_FOUND = "file not found"; static constexpr const char* FOPEN_MSG_OPEN_FAILED = "open failed"; +#ifndef _WIN32 +extern "C" int KeepHostStandardError_nid_no_patch(); +#endif + class FileStream { // Only the macro-accessed FreeBSD prefix is exposed. The remaining guest // FILE area is reserved; host state lives beyond it and is never a guest FILE*. @@ -77,9 +83,13 @@ public: #ifdef _WIN32 std::wstring wideMode; for (const char character : std::string_view(mode)) wideMode.push_back(static_cast(character)); - GuestFileLockForget_nid_no_patch(::_fileno(previous)); + const int replaced = previous ? _fileno(previous) : -1; + auto* const standard = HostStandardHandle_nid_no_patch(replaced); + GuestFileLockForget_nid_no_patch(replaced); _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + KeepStandardHandle_nid_no_patch(replaced, standard); #else + if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); _handle = std::freopen(filename.c_str(), mode, previous); #endif if (!_handle) return false; diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index 4bec836c87..8962c34b16 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -23,6 +23,8 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesFile); + inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved) { try { resolved = ResolvePath_nid_no_patch(path); diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 7de9ed9679..654f5c78c3 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -12,10 +12,16 @@ extern "C" void NotImplemented_nid_no_patch(const char* funcName); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); +#ifndef _WIN32 +extern "C" int KeepHostStandardError_nid_no_patch(); +extern "C" int HostStandardError_nid_no_patch(); +#endif extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); +extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path); extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved); extern "C" bool FinalPathComponentIsDot_nid_no_patch(const char* path); +extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to); // Guest path aliases: a guest prefix such as "/savedata0" (a save-data mount point) resolves to a host // directory instead of the run directory; the prefix matches whole path components only. extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath); @@ -26,6 +32,8 @@ extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); #ifdef _WIN32 extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); +extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); +extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index 2640ee4ee2..ce42a0be61 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -21,7 +21,33 @@ struct Range { bool gpu = false; }; -using Lease = std::vector>; +class Lease { +public: + using Ranges = std::vector>; + using value_type = Ranges::value_type; + using const_iterator = Ranges::const_iterator; + using iterator = const_iterator; + + Lease() = default; + explicit Lease(std::shared_ptr ranges) : ranges(std::move(ranges)) {} + + const_iterator begin() const { return held().begin(); } + const_iterator end() const { return held().end(); } + std::size_t size() const { return held().size(); } + bool empty() const { return held().empty(); } + const value_type& operator[](std::size_t index) const { return held()[index]; } + const value_type& front() const { return held().front(); } + const value_type& back() const { return held().back(); } + void clear() { ranges.reset(); } + +private: + const Ranges& held() const { + static const Ranges none; + return ranges != nullptr ? *ranges : none; + } + + std::shared_ptr ranges; +}; using Mapped = std::vector>; extern "C" { @@ -43,6 +69,7 @@ void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, st void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply); Lease GuestAllocationsAcquire_nid_postfix(); Lease GuestAllocationsAcquireAll_nid_postfix(); +std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next); // Changes whenever a mutation ends, so callers can cache facts about guest mappings between changes. std::uint64_t GuestAllocationsGeneration_nid_postfix(); // Precise invalidation for such caches: the callback runs, after the generation changed, for every @@ -56,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 b42cd1951e..5029a7717a 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -34,6 +34,8 @@ 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); bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); +bool GuestArenaCollectArmedWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); +bool GuestArenaCollectFreshWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes); std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); diff --git a/core/libs/prx/libc/include/GuestDirectory.hpp b/core/libs/prx/libc/include/GuestDirectory.hpp index 532db73fbe..092289d03a 100644 --- a/core/libs/prx/libc/include/GuestDirectory.hpp +++ b/core/libs/prx/libc/include/GuestDirectory.hpp @@ -1,4 +1,5 @@ #pragma once +#include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -12,3 +13,5 @@ struct GuestDirectoryEntry { }; static_assert(offsetof(GuestDirectoryEntry, name) == 8); static_assert(sizeof(GuestDirectoryEntry) == 264); + +extern "C" void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)); diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 87ef0e6bdc..f3deb658df 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -4,6 +4,7 @@ #include "SceTypes.hpp" #include #include +#include #include #include #include @@ -154,6 +155,15 @@ inline void AppendUtf8(std::string& utf8, char32_t code) { } } +inline std::string NanSpec(const std::string& spec) { + std::string result = "%"; + std::size_t index = 1; + for (; index < spec.size() && std::strchr("-+ #0", spec[index]); ++index) + if (spec[index] == '-' && result.size() == 1) result += '-'; + for (; index < spec.size() && spec[index] >= '0' && spec[index] <= '9'; ++index) result += spec[index]; + return result + 's'; +} + inline std::string FormatPointer(const std::string& spec, const void* pointer) { std::size_t index = 1; bool left = false; @@ -327,13 +337,18 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo spec = spec.substr(0, spec.find('.')) + ".1"; output.Value(spec + "ll" + conversion, value); } else if (std::strchr("aAeEfFgG", conversion)) { + const char* nan = conversion >= 'a' ? "nan" : "NAN"; if (length == "L") { static_assert(sizeof(long double) == 16); static_assert(std::numeric_limits::digits == 64); - output.Value(spec + "L" + conversion, args.Next()); + const long double value = args.Next(); + if (std::isnan(value)) output.Value(NanSpec(spec), nan); + else output.Value(spec + "L" + conversion, value); } else { if (!length.empty() && length != "l") throw std::invalid_argument("Invalid floating length"); - output.Value(spec + conversion, args.Next()); + const double value = args.Next(); + if (std::isnan(value)) output.Value(NanSpec(spec), nan); + else output.Value(spec + conversion, value); } } else if (conversion == 'c' && (length.empty() || length == "h")) { output.Value(spec + conversion, args.Next()); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index acb82476a5..d93c344e62 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -4,6 +4,7 @@ #ifdef _WIN32 #include #include +#include #include #include #include @@ -19,6 +20,8 @@ namespace GuestArena { class WindowsMappings { public: + enum class Collection { All, Armed, Fresh }; + static WindowsMappings& Get() { static WindowsMappings mappings; return mappings; @@ -55,7 +58,7 @@ public: const auto mapped = views.find(cursor & ~(pageBytes - 1)); if (mapped != views.end()) { mapped->second.protection = protection; - mapped->second.armed = false; + mapped->second.armed = 0; invalidate(*mapped->second.page); } DWORD previous; @@ -95,7 +98,8 @@ public: } const auto base = cursor + done; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, 0, false, owned, offset + done, 0}); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0}); + remember(freshRanges, base, base + pageBytes); invalidate(*shared); } } @@ -104,7 +108,7 @@ public: std::lock_guard lock(mutex); for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; - it->second.armed = false; + it->second.armed = 0; invalidate(*it->second.page); } } @@ -116,10 +120,9 @@ public: ++page.pins; for (const auto alias : page.aliases) { auto& view = views.at(alias); - if (!view.armed) continue; - DWORD previous; - if (!VirtualProtect(reinterpret_cast(alias), pageBytes, view.protection, &previous)) fail("pin shared guest page writable"); - view.armed = false; + if (view.armed == 0) continue; + protectSubpages(alias, view.armed, view.protection, "pin shared guest page writable"); + view.armed = 0; } invalidate(page); } @@ -140,10 +143,10 @@ public: const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; - invalidate(*view.page); - DWORD previous; - if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); - view.armed = false; + const auto written = subpagesOf(base, address, address + 1); + invalidate(*view.page, written); + protectSubpages(base, written, view.protection, "resume shared memory write"); + view.armed &= static_cast(~written); return true; } @@ -157,11 +160,12 @@ public: for (auto it = first; it != views.end() && it->first < end; ++it) { auto& view = it->second; ++view.hostWrites; - invalidate(*view.page); - if (!view.armed) continue; - DWORD previous; - if (!VirtualProtect(reinterpret_cast(it->first), pageBytes, view.protection, &previous)) fail("open shared memory to a host write"); - view.armed = false; + const auto written = subpagesOf(it->first, address, end); + invalidate(*view.page, written); + const auto opened = static_cast(view.armed & written); + if (opened == 0) continue; + protectSubpages(it->first, opened, view.protection, "open shared memory to a host write"); + view.armed &= static_cast(~opened); } return true; } @@ -171,7 +175,7 @@ public: const auto end = address + bytes; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { --it->second.hostWrites; - invalidate(*it->second.page); + invalidate(*it->second.page, subpagesOf(it->first, address, end)); } } @@ -189,10 +193,13 @@ public: const auto offset = view->second.offset; SYSTEM_INFO system{}; GetSystemInfo(&system); - for (std::size_t done = 0; done < bytes; done += pageBytes, ++view) { - if (view == views.end() || view->first != address + done || view->second.offset != offset + done) throw refuse("the range is not one contiguous run of views of a section"); - if (view->second.section != section && !sameSection(view->second.section->handle, section->handle)) throw refuse("the range spans several sections"); + bool contiguous = true; + for (std::size_t done = 0; done < bytes; done += pageBytes) { + const auto page = views.find(address + done); + if (page == views.end()) throw refuse("the range is not a run of shared views"); + if (page->second.offset != offset + done || (page->second.section != section && !sameSection(page->second.section->handle, section->handle))) contiguous = false; } + if (!contiguous) return mapAliasRuns(address, bytes); const auto lead = offset % system.dwAllocationGranularity; void* alias = map(section->handle, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); if (alias == nullptr) { @@ -205,6 +212,15 @@ public: void UnmapAlias(void* alias) { if (alias == nullptr) return; + { + std::lock_guard lock(mutex); + if (const auto found = splitAliases.find(reinterpret_cast(alias)); found != splitAliases.end()) { + const auto error = releaseAlias(found->first, found->second); + splitAliases.erase(found); + if (error != ERROR_SUCCESS) throw std::system_error(static_cast(error), std::system_category(), "unmap a run of a shared guest alias"); + return; + } + } SYSTEM_INFO system{}; GetSystemInfo(&system); const auto base = reinterpret_cast(alias) & ~(static_cast(system.dwAllocationGranularity) - 1); @@ -219,12 +235,21 @@ public: return true; } - bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear, Collection collection = Collection::All) { std::lock_guard lock(mutex); const auto capacity = *count; *count = 0; const auto end = address + bytes; - if (clear && !collectable(address, end)) return false; + if (collection == Collection::Fresh) { + 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.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; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -238,31 +263,86 @@ public: const auto found = views.find(base); if (found != views.end()) { auto& view = found->second; - const auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); + auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); + if (collection != Collection::All) { + const auto selected = [&](std::uintptr_t at) { + const bool collected = (view.collected & (1u << ((at - base) / 4096))) != 0; + return collection == Collection::Armed ? collected : !collected; + }; + while (cursor < stop && !selected(cursor)) cursor = std::min(stop, cursor + 4096); + if (cursor == stop) continue; + for (auto at = cursor + 4096; at < stop; at += 4096) { + if (!selected(at)) { + stop = at; + break; + } + } + } if (view.protection == PAGE_NOACCESS) return false; const bool pinned = view.page->pins != 0; - if (pinned || view.seen != view.page->generation) { - const auto needed = (stop - cursor + 4095) / 4096; - if (needed > capacity - *count) { - for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast(at); - return true; + std::uint8_t settled = 0; + auto settledEnd = cursor; + bool full = false; + for (std::size_t sub = 0; sub < subpages; ++sub) { + const auto from = std::max(cursor, base + sub * subpageBytes); + const auto to = std::min(stop, base + (sub + 1) * subpageBytes); + if (from >= to) continue; + if (collection == Collection::Fresh || pinned || view.seen[sub] != view.page->generation[sub]) { + if (*count == capacity) { + full = true; + break; + } + pages[(*count)++] = reinterpret_cast(from); } - for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast(at); + settled |= static_cast(1u << sub); + settledEnd = to; } - if (clear && !pinned) { + if (clear) { + for (std::size_t sub = 0; sub < subpages; ++sub) { + const auto bit = static_cast(1u << sub); + if ((settled & bit) == 0 || (view.collected & bit) != 0) continue; + view.collected |= bit; + forget(freshRanges, base + sub * subpageBytes, base + (sub + 1) * subpageBytes); + } + } + if (clear && !pinned && settled != 0) { + std::uint8_t untracked = 0; for (const auto alias : view.page->aliases) { auto& other = views.at(alias); - if (!writable(other.protection) || other.armed || other.hostWrites != 0) continue; - DWORD previous; + const auto arming = static_cast(settled & ~other.armed); + if (!writable(other.protection) || arming == 0) continue; + if (other.hostWrites != 0) { + untracked |= arming; + continue; + } const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; - if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); - other.armed = true; + protectSubpages(alias, arming, protection, "arm shared memory write tracking"); + other.armed |= arming; } - if (cursor == base && stop == base + pageBytes) view.seen = view.page->generation; - rememberClean(cursor, stop); + const auto tracked = static_cast(settled & ~untracked); + auto cleanFrom = cursor; + auto cleanTo = cursor; + for (std::size_t sub = 0; sub < subpages; ++sub) { + if ((tracked & (1u << sub)) == 0) continue; + const auto from = std::max(cursor, base + sub * subpageBytes); + const auto to = std::min(settledEnd, base + (sub + 1) * subpageBytes); + if (from == base + sub * subpageBytes && to == base + (sub + 1) * subpageBytes) view.seen[sub] = view.page->generation[sub]; + if (from != cleanTo) { + if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); + cleanFrom = from; + } + cleanTo = to; + } + if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); } + if (full) return true; cursor = stop; } else { + if (collection == Collection::Fresh) { + const auto next = views.lower_bound(cursor); + cursor = next == views.end() ? end : std::min(end, next->first); + continue; + } 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); @@ -295,8 +375,16 @@ private: } static constexpr std::size_t pageBytes = 0x4000; + static constexpr std::size_t subpageBytes = 0x1000; + 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; struct SharedPage { - std::uint64_t generation = 1; + std::array generation = [] { + std::array initial{}; + initial.fill(1); + return initial; + }(); std::vector aliases; std::uint32_t pins = 0; }; @@ -310,38 +398,75 @@ private: struct View { std::shared_ptr page; DWORD protection; - std::uint64_t seen; - bool armed; + std::array seen; + std::uint8_t armed; std::shared_ptr
section; std::uint64_t offset; std::uint32_t hostWrites; + std::uint8_t collected = 0; }; - void forgetClean(std::uintptr_t start, std::uintptr_t end) { - auto it = cleanRanges.lower_bound(start); - if (it != cleanRanges.begin() && std::prev(it)->second > start) --it; - while (it != cleanRanges.end() && it->first < end) { + using Ranges = std::map; + + 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; + while (it != ranges.end() && it->first < end) { const auto first = it->first; const auto last = it->second; - it = cleanRanges.erase(it); - if (first < start) cleanRanges.emplace(first, start); - if (last > end) it = cleanRanges.emplace(end, last).first; + it = ranges.erase(it); + if (first < start) ranges.emplace(first, start); + if (last > end) it = ranges.emplace(end, last).first; } } + static void remember(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; + while (it != ranges.end() && it->first <= end) { + start = std::min(start, it->first); + end = std::max(end, it->second); + it = ranges.erase(it); + } + ranges.emplace(start, end); + } + + void forgetClean(std::uintptr_t start, std::uintptr_t end) { + forget(cleanRanges, start, end); + } + void rememberClean(std::uintptr_t start, std::uintptr_t end) { - auto it = cleanRanges.lower_bound(start); - if (it != cleanRanges.begin() && std::prev(it)->second >= start) --it; - while (it != cleanRanges.end() && it->first <= end) { - start = std::min(start, it->first); - end = std::max(end, it->second); - it = cleanRanges.erase(it); - } - cleanRanges.emplace(start, end); + remember(cleanRanges, start, end); } - void invalidate(SharedPage& page) { - ++page.generation; - for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes); + void invalidate(SharedPage& page, std::uint8_t mask = allSubpages) { + for (std::size_t sub = 0; sub < subpages; ++sub) { + if ((mask & (1u << sub)) == 0) continue; + ++page.generation[sub]; + for (const auto alias : page.aliases) forgetClean(alias + sub * subpageBytes, alias + (sub + 1) * subpageBytes); + } + } + + static std::uint8_t subpagesOf(std::uintptr_t base, std::uintptr_t address, std::uintptr_t end) { + const auto from = std::max(address, base); + const auto to = std::min(end, base + pageBytes); + if (to <= from) return 0; + const auto first = (from - base) / subpageBytes; + const auto last = (to - 1 - base) / subpageBytes; + return static_cast(((1u << (last + 1)) - 1) & ~((1u << first) - 1)); + } + + static void protectSubpages(std::uintptr_t base, std::uint8_t mask, DWORD protection, const char* operation) { + for (std::size_t sub = 0; sub < subpages;) { + if ((mask & (1u << sub)) == 0) { + ++sub; + continue; + } + auto stop = sub; + while (stop < subpages && (mask & (1u << stop)) != 0) ++stop; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base + sub * subpageBytes), (stop - sub) * subpageBytes, protection, &previous)) fail(operation); + sub = stop; + } } bool sameSection(HANDLE first, HANDLE second) const { @@ -412,6 +537,7 @@ private: if (found != views.end()) { std::erase(found->second.page->aliases, cursor); views.erase(found); + forget(freshRanges, cursor, cursor + pageBytes); } } else if (memory.Type == MEM_PRIVATE) { if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); @@ -428,9 +554,47 @@ private: if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); } - std::map cleanRanges; + void* mapAliasRuns(std::uintptr_t address, std::size_t bytes) { + auto* reserved = allocate(GetCurrentProcess(), nullptr, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); + if (reserved == nullptr) fail("reserve a read-write alias of shared guest memory"); + const auto base = reinterpret_cast(reserved); + try { + for (std::size_t done = 0; done < bytes;) { + const auto& first = views.at(address + done); + auto run = pageBytes; + while (done + run < bytes) { + const auto& next = views.at(address + done + run); + if (next.offset != first.offset + run || (next.section != first.section && !sameSection(next.section->handle, first.section->handle))) break; + run += pageBytes; + } + split(base + done, run); + if (!map(first.section->handle, GetCurrentProcess(), reinterpret_cast(base + done), first.offset, run, MEM_REPLACE_PLACEHOLDER, PAGE_READWRITE, nullptr, 0)) fail("map a run of a read-write alias of shared guest memory"); + done += run; + } + } catch (...) { + releaseAlias(base, bytes); + throw; + } + splitAliases.emplace(base, bytes); + return reserved; + } + + DWORD releaseAlias(std::uintptr_t base, std::size_t bytes) { + DWORD error = ERROR_SUCCESS; + for (auto cursor = base; cursor < base + bytes;) { + const auto memory = query(cursor); + const auto released = memory.State == MEM_RESERVE ? VirtualFree(memory.BaseAddress, 0, MEM_RELEASE) : unmap(GetCurrentProcess(), memory.BaseAddress, 0); + if (!released && error == ERROR_SUCCESS) error = GetLastError(); + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return error; + } + + Ranges cleanRanges; + Ranges freshRanges; std::map views; std::map, std::weak_ptr> physical; + std::map splitAliases; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/prx/libc/include/exceptions/Unwind.hpp b/core/libs/prx/libc/include/exceptions/Unwind.hpp index 07d03fd3f1..bb7f08357c 100644 --- a/core/libs/prx/libc/include/exceptions/Unwind.hpp +++ b/core/libs/prx/libc/include/exceptions/Unwind.hpp @@ -9,7 +9,10 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/specifics/itanium/UnwindAbi.hpp" +extern "C" const std::uintptr_t* UnwindCursorSlots_nid_no_patch(); + struct _Unwind_Context { + const std::uintptr_t* cursor = UnwindCursorSlots_nid_no_patch(); std::uintptr_t registers[17] {}; #ifdef _WIN32 std::uintptr_t vectorRegisters[16][2] {}; @@ -95,6 +98,135 @@ inline unsigned EncodingSize(Byte encoding) { default: std::abort(); } } + +struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; }; + +struct Frame { + const Byte* cieBegin {}; + const Byte* cieEnd {}; + const Byte* begin {}; + const Byte* end {}; + Word start {}, length {}, personality {}, lsda {}; + Word codeAlign {}; + std::intptr_t dataAlign {}; + unsigned returnRegister {}; + bool signal {}; +#ifdef _WIN32 + bool foreign {}; + _Unwind_Context caller {}; +#endif +}; + +inline bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) { + if (!query.fde) return false; + p = query.fde; + auto length = Read(p); + if (!length || length == 0xffffffff) return false; + frame.end = p + length; + const Byte* ciePointer = p; + auto cieOffset = Read(p); + const Byte* cie = ciePointer - cieOffset; + const Byte* c = cie; + auto cieLength = Read(c); + frame.cieEnd = c + cieLength; + if (Read(c) != 0) return false; + Byte version = *c++; + if (version != 1 && version != 3 && version != 4) return false; + augmentation = reinterpret_cast(c); + while (*c++) {} + if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false; + frame.codeAlign = Uleb(c); + frame.dataAlign = Sleb(c); + frame.returnRegister = version == 1 ? *c++ : Uleb(c); +#ifdef _WIN32 + if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false; +#else + if (frame.returnRegister >= 17) return false; +#endif + Byte pointerEncoding = 0; + lsdaEncoding = 255; + if (*augmentation == 'z') { + Word size = Uleb(c); + const Byte* end = c + size; + for (const char* a = augmentation + 1; *a; ++a) { + switch (*a) { + case 'R': pointerEncoding = *c++; break; + case 'L': lsdaEncoding = *c++; break; + case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; } + case 'S': frame.signal = true; break; + default: return false; + } + } + c = end; + } else if (*augmentation) return false; + frame.cieBegin = c; + frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text); + frame.length = Encoded(p, pointerEncoding & 15); + return true; +} + +struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; }; +#ifdef _WIN32 +constexpr unsigned RuleCount = 33; +#else +constexpr unsigned RuleCount = 17; +#endif + +struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; }; + +inline bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) { + Word location = frame.start; + Rules saved[16]; + unsigned depth = 0; + while (p < end && location <= target) { + Byte opcode = *p++; + unsigned reg; + if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; } + if ((opcode & 192) == 128) { + reg = opcode & 63; + auto offset = Uleb(p) * frame.dataAlign; + if (reg < RuleCount) state.registers[reg] = {1, static_cast(offset)}; + continue; + } + if ((opcode & 192) == 192) { + reg = opcode & 63; + if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; + continue; + } + switch (opcode) { + case 0: break; + case 1: location = Read(p); break; + case 2: location += Read(p) * frame.codeAlign; break; + case 3: location += Read(p) * frame.codeAlign; break; + case 4: location += Read(p) * frame.codeAlign; break; + case 5: case 17: case 20: case 21: { + reg = Uleb(p); + auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign; + if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset}; + break; + } + case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break; + case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break; + case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; } + case 10: if (depth == 16) return false; saved[depth++] = state; break; + case 11: if (!depth) return false; state = saved[--depth]; break; + case 12: case 18: + state.cfaExpression = nullptr; state.cfaRegister = Uleb(p); + state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break; + case 13: state.cfaRegister = Uleb(p); break; + case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break; + case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; } + case 16: case 22: { + reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size; + if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr}; + break; + } + case 46: Uleb(p); break; + default: return false; + } + } + return true; +} } extern "C" { 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/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index c8e7e0801c..2553a80f7c 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -62,6 +62,9 @@ void* APS5_VABI __cxa_demangle_nid_postfix(const char* mangled, char* buf, std:: } int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { +#ifdef _WIN32 + dso = nullptr; +#endif struct ThreadAtexitContext { void (APS5_VABI *destructor)(void*); void* object; @@ -83,13 +86,12 @@ int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*) return result; } +int APS5_VABI __cxa_thread_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { + return __cxa_thread_atexit_impl_nid_postfix(func, arg, dso); +} + int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (APS5_VABI *destructor)(void*), void* object, void* module_id) { -#ifdef _WIN32 - (void)module_id; - return __cxa_thread_atexit_impl_nid_postfix(destructor, object, nullptr); -#else return __cxa_thread_atexit_impl_nid_postfix(destructor, object, module_id); -#endif } const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 27c7332b1a..ff24e15a7e 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace { int DirectoryError(int error) { @@ -57,6 +58,8 @@ struct Directory { std::filesystem::path path; GuestDirectoryEntry entry{}; std::mutex mutex; + int descriptor = -1; + int (APS5_VABI* closeDescriptor)(int) = nullptr; ~Directory() { if (native) CloseNative(native); } }; std::uint8_t DirectoryType(const std::filesystem::path& path) { @@ -72,26 +75,7 @@ std::uint8_t DirectoryType(const std::filesystem::path& path) { default: return 0; } } -} - -extern "C" { -void* APS5_VABI opendir_nid_postfix(const char* path) { - if (!path) { errno = 14; return nullptr; } - if (!*path) { errno = 2; return nullptr; } - try { - auto directory = std::make_unique(); - if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } - directory->native = OpenNative(directory->path); - if (!directory->native) { errno = DirectoryError(errno); return nullptr; } - return directory.release(); - } catch (const std::bad_alloc&) { errno = 12; return nullptr; } - catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } -} - -GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { - if (!handle) { errno = 9; return nullptr; } - auto& directory = *static_cast(handle); - std::lock_guard lock(directory.mutex); +GuestDirectoryEntry* ReadEntry(Directory& directory) { const int savedError = errno; errno = 0; std::filesystem::path component; @@ -116,13 +100,54 @@ GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { } catch (const std::bad_alloc&) { errno = 12; return nullptr; } catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } } +} + +extern "C" { +void* APS5_VABI opendir_nid_postfix(const char* path) { + if (!path) { errno = 14; return nullptr; } + if (!*path) { errno = 2; return nullptr; } + try { + auto directory = std::make_unique(); + if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } + directory->native = OpenNative(directory->path); + if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + return directory.release(); + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} + +GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void* handle) { + if (!handle) { errno = 9; return nullptr; } + auto& directory = *static_cast(handle); + std::lock_guard lock(directory.mutex); + return ReadEntry(directory); +} + +int APS5_VABI readdir_r_nid_postfix(void* handle, GuestDirectoryEntry* entry, GuestDirectoryEntry** result) { + if (!handle) return 9; + if (!entry || !result) throw std::invalid_argument("readdir_r: null entry or result"); + auto& directory = *static_cast(handle); + const int savedError = errno; + errno = 0; + GuestDirectoryEntry* next = nullptr; + { + std::lock_guard lock(directory.mutex); + next = ReadEntry(directory); + if (next) std::memcpy(entry, next, next->recordLength); + } + if (!next && errno) return errno; + errno = savedError; + *result = next ? entry : nullptr; + return 0; +} int APS5_VABI closedir_nid_postfix(void* handle) { if (!handle) { errno = 9; return -1; } std::unique_ptr directory(static_cast(handle)); - const int result = CloseNative(directory->native); + int result = CloseNative(directory->native); directory->native = nullptr; if (result) errno = DirectoryError(errno); + if (directory->closeDescriptor && directory->closeDescriptor(directory->descriptor) != 0) result = -1; return result; } @@ -132,4 +157,17 @@ void APS5_VABI rewinddir_nid_postfix(void* handle) { std::lock_guard lock(directory.mutex); RewindNative(directory.native); } + +void* APS5_VABI OpenDescriptorDirectory_nid_no_patch(const char* hostPath, int descriptor, int (APS5_VABI* closeDescriptor)(int)) { + try { + auto directory = std::make_unique(); + directory->path = std::filesystem::path(reinterpret_cast(hostPath)); + directory->native = OpenNative(directory->path); + if (!directory->native) { errno = DirectoryError(errno); return nullptr; } + directory->descriptor = descriptor; + directory->closeDescriptor = closeDescriptor; + return directory.release(); + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } + catch (const std::filesystem::filesystem_error&) { errno = 5; return nullptr; } +} } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 01a17905b9..ef219f06bb 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -87,6 +87,35 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin #endif } +#ifdef _WIN32 +static bool AppendThroughDescriptor(int descriptor) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) return false; + if (descriptor <= 2 || GetFileType(handle) != FILE_TYPE_DISK) return true; + const int translation = ::_setmode(descriptor, _O_BINARY); + if (translation == -1 || ::_setmode(descriptor, translation) == -1) return false; + HANDLE copy = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), ©, 0, TRUE, DUPLICATE_SAME_ACCESS)) { + errno = EBADF; + return false; + } + const int appending = ::_open_osfhandle(reinterpret_cast(copy), _O_APPEND | translation); + if (appending < 0) { + CloseHandle(copy); + return false; + } + const int moved = ::_dup2(appending, descriptor); + const int error = errno; + ::_close(appending); + errno = error; + return moved == 0; +} + +static bool IsPipeStream(std::FILE* native) { + return GetFileType(reinterpret_cast(::_get_osfhandle(_fileno(native)))) == FILE_TYPE_PIPE; +} +#endif + using GuestCompare = int (APS5_VABI *)(const void*, const void*); static void SwapElements(unsigned char* left, unsigned char* right, size_t size) { @@ -196,6 +225,7 @@ 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 + if (parsed->append && !AppendThroughDescriptor(descriptor)) return nullptr; auto* native = ::_fdopen(descriptor, nativeMode.c_str()); #else auto* native = ::fdopen(descriptor, nativeMode.c_str()); @@ -345,7 +375,9 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin) { if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) { errno = 22; return -1; } #ifdef _WIN32 - const int result = _fseeki64(GetNativeStream(stream), offset, origin); + auto* native = GetNativeStream(stream); + if (IsPipeStream(native)) { errno = 29; return -1; } + const int result = _fseeki64(native, offset, origin); #else static_assert(sizeof(off_t) == 8); const int result = ::fseeko(GetNativeStream(stream), offset, origin); @@ -358,7 +390,9 @@ int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int or std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream) { #ifdef _WIN32 - const auto result = _ftelli64(GetNativeStream(stream)); + auto* native = GetNativeStream(stream); + if (IsPipeStream(native)) { errno = 29; return -1; } + const auto result = _ftelli64(native); #else static_assert(sizeof(off_t) == 8); const auto result = ::ftello(GetNativeStream(stream)); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index a2a57370b2..133196f578 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -20,6 +22,7 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#include #else #include #include @@ -251,6 +254,38 @@ int SetWorkingDirectory(WorkingDirectory& state, const std::filesystem::path& re } } +#ifndef _WIN32 +namespace { +std::mutex g_hostStandardErrorMutex; +std::atomic g_hostStandardError{-1}; +} + +extern "C" int KeepHostStandardError_nid_no_patch() { + const std::lock_guard lock(g_hostStandardErrorMutex); + if (const int kept = g_hostStandardError.load(); kept >= 0) return kept; + const int kept = ::fcntl(2, F_DUPFD_CLOEXEC, 3); + if (kept < 0) { + if (errno == EBADF) return -1; + throw std::system_error(errno, std::generic_category(), "Keeping the host standard error"); + } + std::FILE* const stream = ::fdopen(kept, "w"); + if (stream == nullptr) { + const int error = errno; + ::close(kept); + throw std::system_error(error, std::generic_category(), "Keeping the host standard error"); + } + std::setvbuf(stream, nullptr, _IONBF, 0); + std::fflush(stderr); + stderr = stream; + g_hostStandardError.store(kept); + return kept; +} + +extern "C" int HostStandardError_nid_no_patch() { + return g_hostStandardError.load(); +} +#endif + extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { if (guestPrefix == nullptr || hostPath == nullptr) { APS5_INVALID_ARG_EX; @@ -345,6 +380,13 @@ extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { return Resolve(state, path); } +extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path) { + auto relinked = path; + relinked += ".guest.prx"; + std::error_code error; + return std::filesystem::is_regular_file(relinked, error) ? relinked : path; +} + extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { if (resolved == nullptr) return 14; auto& state = Directories(); @@ -434,6 +476,28 @@ extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { return EINVAL; } } + +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +static DWORD StandardHandleId(int descriptor) { + return descriptor == 0 ? STD_INPUT_HANDLE : descriptor == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE; +} +extern "C" void* HostStandardHandle_nid_no_patch(int descriptor) { + if (descriptor < 0 || descriptor > 2) return nullptr; + const HANDLE current = GetStdHandle(StandardHandleId(descriptor)); + if (current == nullptr || current == INVALID_HANDLE_VALUE) return nullptr; + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const auto owned = ::_get_osfhandle(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + if (reinterpret_cast(owned) != current) return current; + HANDLE kept = nullptr; + if (!DuplicateHandle(GetCurrentProcess(), current, GetCurrentProcess(), &kept, 0, TRUE, DUPLICATE_SAME_ACCESS)) + throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); + return kept; +} +extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { + if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) + throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); +} #endif extern "C" void NotImplemented_nid_no_patch(const char* funcName) { diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index f6299305f2..c7b286af87 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -36,6 +36,8 @@ struct Registry { std::map> ranges; bool mainImageRegistered = false; std::map registeredImages; + std::weak_ptr readableLease; + std::weak_ptr allLease; }; Registry& registry() { @@ -47,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() { @@ -109,7 +112,10 @@ bool backWritableImageAnonymously() { } void* GuestAllocationsBegin_nid_postfix() { - return new MutationState{std::unique_lock(registry().mutex), {}}; + auto* state = new MutationState{std::unique_lock(registry().mutex), {}}; + registry().readableLease.reset(); + registry().allLease.reset(); + return state; } void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { @@ -120,7 +126,7 @@ void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { } if (!state->gpuMapped.empty()) gpuMapObserverCalls.fetch_add(1); const auto observer = state->gpuMapped.empty() ? nullptr : gpuMapObserver.load(); - Lease mapped; + Lease::Ranges mapped; if (observer != nullptr) { const auto& ranges = registry().ranges; for (const auto& [address, bytes] : state->gpuMapped) { @@ -137,6 +143,8 @@ void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept { const Mapped weak(mapped.begin(), mapped.end()); mapped.clear(); const bool noted = !state->gpuMapped.empty(); + registry().readableLease.reset(); + registry().allLease.reset(); delete state; if (!weak.empty()) observer(weak, ended); if (noted) gpuMapObserverCalls.fetch_sub(1); @@ -312,6 +320,8 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; for (;;) { + registry().readableLease.reset(); + registry().allLease.reset(); bool pinned = false; auto it = ranges.upper_bound(address); if (it != ranges.begin()) --it; @@ -373,10 +383,14 @@ bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); const auto end = address + bytes; auto cursor = address; - for (const auto& [base, range] : registry().ranges) { - const auto finish = base + range->bytes; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end(); ++it) { + const auto base = it->first; + const auto finish = base + it->second->bytes; if (finish <= cursor) continue; - if (base > cursor || !range->releasable) return false; + if (base > cursor || !it->second->releasable) return false; cursor = finish; if (cursor >= end) return true; } @@ -408,29 +422,39 @@ 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 { -std::map> replaceRange(const void* pointer, std::size_t bytes, bool remove, bool readable, bool writable, bool gpu) { +struct RangePatch { + std::vector erased; + std::vector>> inserted; +}; + +RangePatch replaceRange(const void* pointer, std::size_t bytes, bool remove, bool readable, bool writable, bool gpu) { const auto address = reinterpret_cast(pointer); require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest protection or unmap range"); require(!writable || readable, "writable guest allocation must be readable"); const auto end = address + bytes; - auto replacement = registry().ranges; + RangePatch patch; auto cursor = address; - for (const auto& [base, entry] : registry().ranges) { - const auto& range = *entry; + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) --it; + for (; it != ranges.end(); ++it) { + const auto base = it->first; + const auto& range = *it->second; const auto finish = base + range.bytes; if (finish <= address) continue; if (base >= end) break; require(base <= cursor, "guest protection or unmap range has a hole"); - replacement.erase(base); + patch.erased.push_back(base); const auto insert = [&](std::uint64_t first, std::uint64_t last, bool canRead, bool canWrite, bool gpuMapped) { - if (first < last) replacement.emplace(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped})); + if (first < last) patch.inserted.emplace_back(first, std::make_shared(Range{first, static_cast(last - first), canRead, canWrite, range.allocationAddress, range.allocationBytes, range.releasable, gpuMapped})); }; insert(base, std::max(base, address), range.readable, range.writable, range.gpu); if (!remove) insert(std::max(base, address), std::min(finish, end), readable, writable, gpu); @@ -438,7 +462,26 @@ std::map> replaceRange(const void* p cursor = std::min(finish, end); } require(cursor == end, "guest protection or unmap range is not registered"); - return replacement; + return patch; +} + +void applyPatch(const RangePatch& patch) { + auto& ranges = registry().ranges; + for (const auto key : patch.erased) ranges.erase(key); + for (const auto& [key, range] : patch.inserted) ranges.emplace(key, range); +} + +bool allocationRemains(const Range& range, const RangePatch& patch) { + for (const auto& [key, entry] : patch.inserted) { + if (entry->allocationAddress == range.allocationAddress) return true; + } + const auto& ranges = registry().ranges; + const auto limit = range.allocationAddress + range.allocationBytes; + for (auto it = ranges.lower_bound(range.allocationAddress); it != ranges.end() && it->first < limit; ++it) { + if (it->second->allocationAddress != range.allocationAddress) continue; + if (std::find(patch.erased.begin(), patch.erased.end(), it->first) == patch.erased.end()) return true; + } + return false; } } @@ -446,9 +489,9 @@ std::map> replaceRange(const void* p void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu, const std::function& apply) { GuestAllocationsRequireUnpinned_nid_postfix(mutation, pointer, bytes); recordChange(mutation, pointer, bytes); - auto replacement = replaceRange(pointer, bytes, false, readable, writable, gpu); + const auto patch = replaceRange(pointer, bytes, false, readable, writable, gpu); apply(); - registry().ranges.swap(replacement); + applyPatch(patch); } void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, const std::function& apply) { @@ -472,36 +515,59 @@ void GuestAllocationsUnmap_nid_postfix(void* mutation, const void* pointer, std: const auto range = *containing; require(range.releasable, "guest image memory cannot be unmapped"); const auto pieceEnd = std::min(end, range.address + range.bytes); - auto replacement = replaceRange(reinterpret_cast(cursor), pieceEnd - cursor, true, false, false, false); - bool last = true; - for (const auto& [base, entry] : replacement) { - if (entry->allocationAddress == range.allocationAddress) last = false; - } + const auto patch = replaceRange(reinterpret_cast(cursor), pieceEnd - cursor, true, false, false, false); + const bool last = !allocationRemains(range, patch); apply(reinterpret_cast(cursor), pieceEnd - cursor, reinterpret_cast(range.allocationAddress), last); - registry().ranges.swap(replacement); + 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() { std::lock_guard lock(registry().mutex); - Lease result; + if (auto shared = registry().readableLease.lock()) return Lease(std::move(shared)); + auto ranges = std::make_shared(); for (const auto& [address, range] : registry().ranges) { - if (range->readable && range->bytes != 0) result.push_back(range); + if (range->readable && range->bytes != 0) ranges->push_back(range); } - return result; + registry().readableLease = ranges; + return Lease(std::move(ranges)); +} + +std::shared_ptr GuestAllocationsLookup_nid_postfix(std::uintptr_t address, bool next) { + std::lock_guard lock(registry().mutex); + const auto& ranges = registry().ranges; + auto it = ranges.upper_bound(address); + if (it != ranges.begin()) { + const auto& previous = std::prev(it)->second; + if (previous->bytes != 0 && address < previous->address + previous->bytes) return previous; + } + if (next) { + for (; it != ranges.end(); ++it) { + if (it->second->bytes != 0) return it->second; + } + } + return nullptr; } Lease GuestAllocationsAcquireAll_nid_postfix() { std::lock_guard lock(registry().mutex); - Lease result; - result.reserve(registry().ranges.size()); + if (auto shared = registry().allLease.lock()) return Lease(std::move(shared)); + auto ranges = std::make_shared(); + ranges->reserve(registry().ranges.size()); for (const auto& [address, range] : registry().ranges) { - if (range->bytes != 0) result.push_back(range); + if (range->bytes != 0) ranges->push_back(range); } - return result; + registry().allLease = ranges; + return Lease(std::move(ranges)); } } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 025f4faaa9..5ff89ebc9c 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -338,6 +338,14 @@ bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t byt return WindowsMappings::Get().Collect(address, bytes, pages, count, clear); } +bool GuestArenaCollectArmedWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear, WindowsMappings::Collection::Armed); +} + +bool GuestArenaCollectFreshWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear, WindowsMappings::Collection::Fresh); +} + bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes) { return Arena::Get().OverlapsHostRegion(address, bytes); } diff --git a/core/libs/prx/libc/src/HeapDiagnostics.cpp b/core/libs/prx/libc/src/HeapDiagnostics.cpp index 39aaf39511..135c46f928 100644 --- a/core/libs/prx/libc/src/HeapDiagnostics.cpp +++ b/core/libs/prx/libc/src/HeapDiagnostics.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace { @@ -28,18 +29,14 @@ void LibcHeapTraceInfo_nid_no_patch(LibcHeapInfo* info) { info->mstate_table = mstateTable.data(); } -// unknown signature void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName) { - (void)heapName; - NotImplemented_nid_no_patch(__func__); + std::fprintf(stderr, "[%s] backtrace requested from guest 0x%llx\n", heapName ? heapName : "", + static_cast(reinterpret_cast(__builtin_return_address(0)))); } -// unknown signature void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error) { - (void)heap; - (void)block; - (void)error; - NotImplemented_nid_no_patch(__func__); + std::fprintf(stderr, "[libc] heap error 0x%x: heap %p, block %p, reported from guest 0x%llx\n", error, heap, block, + static_cast(reinterpret_cast(__builtin_return_address(0)))); } } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a21ce82275..9d70e10e48 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -81,12 +82,24 @@ GuestLocale::Facet* g_classicFacets[1]{}; GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; constexpr std::size_t GuestStringInlineCapacity = 15; +constexpr std::size_t GuestStringCapacityMask = 15; constexpr std::size_t GuestStringLargeAllocation = 0x1000; +constexpr std::size_t GuestLargeAllocationPadding = 0x27; +constexpr std::uintptr_t GuestLargeAllocationAlignment = 0x20; void ValidateCharacterRange(const char* first, const char* last, const char* function) { if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); } +void* AllocateStringStorage(std::size_t bytes) { + if (bytes < GuestStringLargeAllocation) return _Znwm_nid_postfix(bytes); + if (bytes > std::numeric_limits::max() - GuestLargeAllocationPadding) throw std::bad_alloc(); + auto* raw = static_cast(_Znwm_nid_postfix(bytes + GuestLargeAllocationPadding)); + const auto aligned = (reinterpret_cast(raw) + GuestLargeAllocationPadding) & ~(GuestLargeAllocationAlignment - 1); + reinterpret_cast(aligned)[-1] = raw; + return reinterpret_cast(aligned); +} + void APS5_VABI DestroyCollate(GuestLocale::Facet*) {} void APS5_VABI DeleteCollate(GuestLocale::Facet* self) { @@ -121,12 +134,11 @@ GuestLocale::String* APS5_VABI CollateTransform(GuestLocale::String* result, con ValidateCharacterRange(first, last, "collate::do_transform"); const auto length = static_cast(last - first); if (length != 0 && std::memchr(first, 0, length) != nullptr) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); - if (length + 1 >= GuestStringLargeAllocation) throw std::runtime_error("collate::do_transform: results of 4095 characters or more are not supported"); char* data = result->buffer; std::size_t capacity = GuestStringInlineCapacity; if (length > GuestStringInlineCapacity) { - capacity = length; - data = static_cast(_Znwm_nid_postfix(capacity + 1)); + capacity = length | GuestStringCapacityMask; + data = static_cast(AllocateStringStorage(capacity + 1)); } if (length != 0) std::memcpy(data, first, length); data[length] = 0; diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9ecf79e147..4b4a78f73d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -79,6 +79,44 @@ float SinhfExp(float x) { return y * std::bit_cast(static_cast(0x7f + k) << 23); } +float AtanhfLog1p(float x) { + constexpr float Ln2High = 6.9313812256e-01f, Ln2Low = 9.0580006145e-06f; + constexpr float Lp1 = 6.6666668653e-01f, Lp2 = 4.0000000596e-01f, Lp3 = 2.8571429849e-01f, Lp4 = 2.2222198546e-01f; + constexpr float Lp5 = 1.8183572590e-01f, Lp6 = 1.5313838422e-01f, Lp7 = 1.4798198640e-01f; + const std::uint32_t bits = std::bit_cast(x); + if (bits < 0x38000000u) return bits < 0x33800000u ? x : x - x * x * 0.5f; + int k = 0; + float f = x; + float c = 0.0f; + std::uint32_t mantissa = 1; + if (bits >= 0x3ed413d0u) { + const float u = 1.0f + x; + const std::uint32_t uBits = std::bit_cast(u); + k = static_cast(uBits >> 23) - 127; + c = (k > 0 ? 1.0f - (u - x) : x - (u - 1.0f)) / u; + mantissa = uBits & 0x007fffffu; + if (mantissa < 0x3504f4u) { + f = std::bit_cast(mantissa | 0x3f800000u) - 1.0f; + } else { + ++k; + f = std::bit_cast(mantissa | 0x3f000000u) - 1.0f; + mantissa = (0x00800000u - mantissa) >> 2; + } + } + const float halfSquare = 0.5f * f * f; + const float kFloat = static_cast(k); + if (mantissa == 0) { + if (f == 0.0f) return kFloat * Ln2High + (c + kFloat * Ln2Low); + const float r = halfSquare * (1.0f - 0.66666666666666666f * f); + return kFloat * Ln2High - ((r - (kFloat * Ln2Low + c)) - f); + } + const float s = f / (2.0f + f); + const float z = s * s; + const float r = z * (Lp1 + z * (Lp2 + z * (Lp3 + z * (Lp4 + z * (Lp5 + z * (Lp6 + z * Lp7)))))); + if (k == 0) return f - (halfSquare - s * (halfSquare + r)); + return kFloat * Ln2High - ((halfSquare - (s * (halfSquare + r) + (kFloat * Ln2Low + c))) - f); +} + static_assert(std::numeric_limits::digits == 64, "guest long double is the x87 80-bit format"); struct ExtendedParts { @@ -144,7 +182,18 @@ float APS5_VABI asinf_nid_postfix(float x) { errno = saved_errno; return result; } -float APS5_VABI acosf_nid_postfix(float x) { return std::acos(x); } +float APS5_VABI acosf_nid_postfix(float x) { + if (std::isnan(x)) { + return std::bit_cast(std::bit_cast(x) | 0x00400000u); + } + if (std::fabs(x) > 1.0f) { + return std::bit_cast(0xffc00000u); + } + const int saved_errno = errno; + const float result = std::acos(x); + errno = saved_errno; + return result; +} float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } double APS5_VABI hypot_nid_postfix(double x, double y) { @@ -176,11 +225,14 @@ std::int64_t APS5_VABI lround_nid_postfix(double x) { return RoundToInt64(x); } std::int64_t APS5_VABI lroundf_nid_postfix(float x) { return RoundToInt64(x); } std::int64_t APS5_VABI llround_nid_postfix(double x) { return RoundToInt64(x); } std::int64_t APS5_VABI llroundf_nid_postfix(float x) { return RoundToInt64(x); } +std::int64_t APS5_VABI llroundl_nid_postfix(long double x) { return RoundToInt64(x); } int APS5_VABI __isfinitef_nid_postfix(float x) { return std::isfinite(x) ? 1 : 0; } int APS5_VABI __isnormal_nid_postfix(double x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0; } int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } float APS5_VABI nearbyintf_nid_postfix(float x) { return std::nearbyintf(x); } +long double APS5_VABI nearbyintl_nid_postfix(long double x) { return std::nearbyint(x); } +double APS5_VABI nearbyint_nid_postfix(double x) { return std::nearbyint(x); } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } double APS5_VABI asin_nid_postfix(double x) { @@ -196,6 +248,11 @@ float APS5_VABI sqrtf_nid_postfix(float x) { return std::sqrt(x); } double APS5_VABI fabs_nid_postfix(double x) { return std::fabs(x); } int APS5_VABI abs_nid_postfix(int x) { return x < 0 ? static_cast(0u - static_cast(x)) : x; } double APS5_VABI trunc_nid_postfix(double x) { return std::trunc(x); } +double APS5_VABI ceil_nid_postfix(double x) { return std::ceil(x); } +double APS5_VABI floor_nid_postfix(double x) { return std::floor(x); } +float APS5_VABI truncf_nid_postfix(float x) { return std::trunc(x); } +float APS5_VABI ceilf_nid_postfix(float x) { return std::ceil(x); } +float APS5_VABI floorf_nid_postfix(float x) { return std::floor(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } @@ -241,6 +298,9 @@ double APS5_VABI log_nid_postfix(double x) { return std::log(x); } double APS5_VABI log1p_nid_postfix(double x) { return std::log1p(x); } float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } +float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } +float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } +double APS5_VABI lgamma_nid_postfix(double x) { return std::lgamma(x); } float APS5_VABI sinf_nid_postfix(float x) { return std::sin(x); } float APS5_VABI cosf_nid_postfix(float x) { return std::cos(x); } @@ -282,6 +342,25 @@ double APS5_VABI acosh_nid_postfix(double x) { return std::acosh(x); } float APS5_VABI acoshf_nid_postfix(float x) { return std::acosh(x); } double APS5_VABI atanh_nid_postfix(double x) { return std::atanh(x); } +float APS5_VABI atanhf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude > 0x3f800000u) { + std::feraiseexcept(FE_INVALID); + errno = EDOM; + return (x - x) / (x - x); + } + if (magnitude == 0x3f800000u) { + errno = ERANGE; + return x / 0.0f; + } + if (magnitude < 0x31800000u && 1.0e30f + x > 0.0f) return x; + const float absolute = std::fabs(x); + const float twice = absolute + absolute; + const float t = magnitude < 0x3f000000u ? 0.5f * AtanhfLog1p(twice + twice * absolute / (1.0f - absolute)) + : 0.5f * AtanhfLog1p(twice / (1.0f - absolute)); + return std::signbit(x) ? -t : t; +} + void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { *sinp = std::sin(x); *cosp = std::cos(x); @@ -289,7 +368,10 @@ void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } -float APS5_VABI powf_nid_postfix(float base, float exp) { return std::pow(base, exp); } +float APS5_VABI powf_nid_postfix(float base, float exp) { + if (base == 1.0f) return 1.0f; + return std::pow(base, exp); +} double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { const bool reciprocal = exponent < 0; @@ -303,6 +385,18 @@ float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { } return reciprocal ? 1.0f / result : result; } +double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { + const bool reciprocal = exponent < 0; + double result = 1.0; + unsigned int magnitude = reciprocal ? 0u - static_cast(exponent) : static_cast(exponent); + while (magnitude != 0u) { + if ((magnitude & 1u) != 0u) result *= base; + magnitude >>= 1u; + if (magnitude == 0u) break; + base *= base; + } + return reciprocal ? 1.0 / result : result; +} float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } @@ -371,8 +465,23 @@ 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); } +int APS5_VABI isinf_nid_postfix(double x) { return __isinf_nid_postfix(x); } int APS5_VABI __fpclassifyf_nid_postfix(float x) { constexpr int Infinite = 0x01, NotANumber = 0x02, Normal = 0x04, Subnormal = 0x08, Zero = 0x10; std::uint32_t bits; @@ -430,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 16e7c57926..be9746f2b7 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -22,7 +22,6 @@ constexpr std::uintptr_t ArenaHeaderBytes = 64; struct Chunk { std::uintptr_t end; bool used; - std::size_t requested; }; struct Arena { @@ -51,7 +50,7 @@ Arena* Find(void* handle) { } void AddFree(Arena& arena, std::uintptr_t start, std::uintptr_t end) { - arena.chunks[start] = {end, false, 0}; + arena.chunks[start] = {end, false}; arena.free.emplace(end - start, start); } @@ -62,7 +61,7 @@ void RemoveFree(Arena& arena, std::map::iterator chunk) { void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (!arena) return nullptr; alignment = std::max(alignment, MinimumAlignment); - const auto needed = AlignUp(std::max(size, 1), Granule); + const auto needed = AlignUp(std::max(size, 1), MinimumAlignment); if (needed < size || needed > std::numeric_limits::max() - alignment) { Error(12); return nullptr; @@ -78,7 +77,7 @@ void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (aligned > start) AddFree(*arena, start, aligned); const auto finish = aligned + needed; if (finish < end) AddFree(*arena, finish, end); - arena->chunks[aligned] = {finish, true, size}; + arena->chunks[aligned] = {finish, true}; arena->inUse += needed; arena->peakInUse = std::max(arena->peakInUse, arena->inUse); return reinterpret_cast(aligned); @@ -216,14 +215,11 @@ void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, st std::map::iterator chunk; if (!FindUsed(arena, pointer, chunk)) return nullptr; if (!size) { Release(*arena, chunk); return nullptr; } - const auto needed = AlignUp(size, Granule); + const auto needed = AlignUp(size, MinimumAlignment); if (needed < size) { Error(12); return nullptr; } const auto start = chunk->first; const auto capacity = chunk->second.end - start; - if (needed <= capacity) { - chunk->second.requested = size; - return pointer; - } + if (needed <= capacity) return pointer; const auto next = std::next(chunk); if (next != arena->chunks.end() && !next->second.used && next->first == chunk->second.end && next->second.end - start >= needed) { const auto nextEnd = next->second.end; @@ -233,13 +229,12 @@ void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, st if (finish < nextEnd) AddFree(*arena, finish, nextEnd); arena->inUse += needed - capacity; arena->peakInUse = std::max(arena->peakInUse, arena->inUse); - arena->chunks[start] = {finish, true, size}; + arena->chunks[start] = {finish, true}; return pointer; } - const auto previousSize = chunk->second.requested; void* result = Allocate(arena, size, Granule); if (result) { - std::memcpy(result, pointer, previousSize); + std::memcpy(result, pointer, std::min(capacity, size)); Release(*arena, arena->chunks.find(start)); } return result; @@ -281,14 +276,11 @@ void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointe const auto needed = AlignUp(size, Granule); if (needed < size) { Error(12); return nullptr; } const auto start = chunk->first; - if (needed <= chunk->second.end - start && (start & (effective - 1)) == 0) { - chunk->second.requested = size; - return pointer; - } - const auto previousSize = chunk->second.requested; + const auto capacity = chunk->second.end - start; + if (needed <= capacity && (start & (effective - 1)) == 0) return pointer; void* result = Allocate(arena, size, effective); if (result) { - std::memcpy(result, pointer, std::min(previousSize, size)); + std::memcpy(result, pointer, std::min(capacity, size)); Release(*arena, arena->chunks.find(start)); } return result; diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index b5bb045276..d7f2126c76 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -3,6 +3,42 @@ #include #include #include +#ifdef _WIN32 +#include +#include +#include +#include +#endif + +std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, [[maybe_unused]] bool replacesFile) { + namespace fs = std::filesystem; + std::error_code error; +#ifdef _WIN32 + if (replacesFile) { + const auto target = fs::absolute(destination, error); + if (error) return error; + struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; + static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); + if (target.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return std::make_error_code(std::errc::filename_too_long); + const auto nameBytes = target.native().size() * sizeof(wchar_t); + std::vector buffer(sizeof(RenameInfo) + nameBytes); + auto* info = reinterpret_cast(buffer.data()); + constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002, ignoreReadOnly = 0x00000040; + const auto attributes = GetFileAttributesW(destination.c_str()); + info->flags = replaceExisting | posixSemantics | (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY) ? ignoreReadOnly : 0); + info->nameBytes = static_cast(nameBytes); + std::memcpy(info->name, target.c_str(), nameBytes); + const auto handle = CreateFileW(source.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr); + if (handle == INVALID_HANDLE_VALUE) return std::error_code(static_cast(GetLastError()), std::system_category()); + constexpr auto renameInfoEx = static_cast(22); + if (!SetFileInformationByHandle(handle, renameInfoEx, info, static_cast(buffer.size()))) error = std::error_code(static_cast(GetLastError()), std::system_category()); + CloseHandle(handle); + return error; + } +#endif + fs::rename(source, destination, error); + return error; +} static int RenamePath_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination) { constexpr int GuestEnoent = 2; @@ -61,12 +97,11 @@ static int RenamePath_nid_no_patch(const std::filesystem::path& source, const st } #endif - error.clear(); - fs::rename(source, destination, error); + error = RenameNative_nid_no_patch(source, destination, destinationExists && !sourceIsDirectory && !destinationIsDirectory); return error ? FilesystemError(error) : 0; } -extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { +extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { if (!from || !to) { errno = 14; return -1; } if (!*from || !*to) { errno = 2; return -1; } try { @@ -83,3 +118,7 @@ extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { return -1; } } + +extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) { + return RenameGuestPath_nid_no_patch(from, to); +} 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/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index 79e53ccacd..ab69378356 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -66,7 +66,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma // Only the "C" locale exists. const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { - (void)category; + if (category < 0 || category > 6) return nullptr; if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0 || std::strcmp(locale, "POSIX") == 0) return "C"; return nullptr; } diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index 8ea34819c8..f1f4eb975d 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -127,11 +127,12 @@ char* APS5_VABI strtok_r_nid_postfix(char* text, const char* delimiters, char** if (text == nullptr) return nullptr; text += std::strspn(text, delimiters); if (*text == '\0') { - *state = text; + *state = nullptr; return nullptr; } char* end = text + std::strcspn(text, delimiters); if (*end != '\0') *end++ = '\0'; + else end = nullptr; *state = end; return text; } diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 27a5c5da80..c0f6747057 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -36,6 +37,33 @@ std::string AsciiPrefix(const char16_t* text) { return prefix; } +bool IsNanSubject(const char* text) { + while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text; + if (*text == '+' || *text == '-') ++text; + return *text == 'n' || *text == 'N'; +} + +double QuietNan(double value, const char* text) { + const auto bits = std::bit_cast(value); + if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value; + return std::bit_cast((bits & 0x8000000000000000u) | 0x7ff8000000000000u); +} + +float QuietNan(float value, const char* text) { + const auto bits = std::bit_cast(value); + if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value; + return std::bit_cast((bits & 0x80000000u) | 0x7fc00000u); +} + +long double QuietNan(long double value, const char* text) { + std::uint16_t exponent; + std::memcpy(&exponent, reinterpret_cast(&value) + sizeof(std::uint64_t), sizeof(exponent)); + if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value; + const std::uint64_t significand = 0xc000000000000000u; + std::memcpy(&value, &significand, sizeof(significand)); + return value; +} + template auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) { const std::string prefix = AsciiPrefix(text); @@ -50,7 +78,7 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha char* parsedEnd = nullptr; const TFloat value = parse(text, &parsedEnd); if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : value; + return parsedEnd == text ? TFloat{0} : QuietNan(value, text); } } @@ -392,6 +420,11 @@ char16_t* APS5_VABI wcscpy_nid_postfix(char16_t* dest, const char16_t* src) { return dest; } +char16_t* APS5_VABI wcscat_nid_postfix(char16_t* dest, const char16_t* src) { + std::memcpy(dest + Length(dest), src, (Length(src) + 1) * sizeof(char16_t)); + return dest; +} + char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, size_t n) { size_t index = 0; for (; index < n && src[index] != 0; ++index) dest[index] = src[index]; @@ -503,7 +536,11 @@ int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second) size_t APS5_VABI wcsxfrm_nid_postfix(char16_t* destination, const char16_t* source, size_t count) { const size_t length = Length(source); - if (length < count) std::memcpy(destination, source, (length + 1) * sizeof(char16_t)); + if (count != 0) { + const size_t copied = length < count ? length : count - 1; + std::memcpy(destination, source, copied * sizeof(char16_t)); + destination[copied] = 0; + } return length; } diff --git a/core/libs/prx/libc/src/Syslog.cpp b/core/libs/prx/libc/src/Syslog.cpp index eb5f721e51..a94f34e141 100644 --- a/core/libs/prx/libc/src/Syslog.cpp +++ b/core/libs/prx/libc/src/Syslog.cpp @@ -4,9 +4,13 @@ #include #include #include +#include #include #ifdef _WIN32 #include "prx/libc/include/WindowsFormatting.hpp" +#include +#else +#include #endif extern "C" { @@ -20,7 +24,11 @@ constexpr int priorityMask = 0x07; constexpr int facilityMask = 0x3f8; constexpr int userFacility = 1 << 3; constexpr int internalLogPriority = 3 | 0x02 | 0x20 | 0x01; +constexpr int logPid = 0x01; int logMask = 0xff; +int logOptions = 0; +int logFacility = userFacility; +std::optional logIdentity; std::mutex& OutputMutex() { static std::mutex mutex; @@ -28,7 +36,17 @@ std::mutex& OutputMutex() { } void Emit(int priority, const std::string& message) { - std::fprintf(stderr, "[syslog:%d] %s", priority, message.c_str()); + std::fprintf(stderr, "[syslog:%d] ", priority); + if (logIdentity) std::fputs(logIdentity->c_str(), stderr); + if (logOptions & logPid) { +#ifdef _WIN32 + std::fprintf(stderr, "[%d]", ::_getpid()); +#else + std::fprintf(stderr, "[%d]", static_cast(::getpid())); +#endif + } + if (logIdentity) std::fputs(": ", stderr); + std::fputs(message.c_str(), stderr); if (message.empty() || message.back() != '\n') std::fputc('\n', stderr); std::fflush(stderr); } @@ -71,6 +89,18 @@ std::string Format(const char* format, VaList* args) { } extern "C" { +void APS5_VABI openlog_nid_postfix(const char* identity, int options, int facility) { + std::lock_guard lock(OutputMutex()); + if (identity) logIdentity = identity; + logOptions = options; + if (facility != 0 && (facility & ~facilityMask) == 0) logFacility = facility; +} + +void APS5_VABI closelog_nid_postfix() { + std::lock_guard lock(OutputMutex()); + logIdentity.reset(); +} + int APS5_VABI setlogmask_nid_postfix(int mask) { std::lock_guard lock(OutputMutex()); const int previous = logMask; @@ -90,7 +120,7 @@ void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* arg priority &= priorityMask | facilityMask; } if (logMask & (1 << (priority & priorityMask))) { - if ((priority & facilityMask) == 0) priority |= userFacility; + if ((priority & facilityMask) == 0) priority |= logFacility; Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); } *__error_nid_postfix() = saved; diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 44db68abea..b1d85f9faf 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,18 +25,18 @@ struct Xtime { constexpr std::int64_t nanosPerSecond = 1000000000; constexpr std::int64_t longestSleepSeconds = 86400; -timespec XtimeDeadline(const Xtime& time) { +timespec XtimeDeadline(const Xtime& time, const char* function) { constexpr auto largest = std::numeric_limits::max(); constexpr auto smallest = std::numeric_limits::min(); const auto carry = time.nsec / nanosPerSecond; auto nanos = time.nsec % nanosPerSecond; auto seconds = time.sec; if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) - throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); seconds += carry; if (nanos < 0) { if (seconds == smallest) - throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); nanos += nanosPerSecond; --seconds; } @@ -44,6 +45,7 @@ timespec XtimeDeadline(const Xtime& time) { constexpr int threadSuccess = 0; constexpr int threadNomem = 1; +constexpr int threadTimedout = 2; constexpr int threadBusy = 3; constexpr int threadError = 4; @@ -107,6 +109,10 @@ int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type) { return threadSuccess; } +int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char*) { + return _Mtx_init_nid_postfix(handle, type); +} + void APS5_VABI _Mtx_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Mtx_destroy: null mutex handle pointer"); @@ -216,6 +222,44 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) return threadSuccess; } +int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait"); + constexpr auto maxDuration = std::chrono::system_clock::duration::max(); + constexpr auto minDuration = std::chrono::system_clock::duration::min(); + constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); + constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); + constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); + if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || + deadline.tv_sec < minSeconds) + throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow"); + auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait"); + auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait"); + if (!IsOwnedByCaller(mutex)) + return threadSuccess; + if (mutex->count != 1) + return threadSuccess; + mutex->count = 0; + mutex->owner.store(std::thread::id{}, std::memory_order_release); + std::unique_lock lock(mutex->native, std::adopt_lock); + bool timeout = false; + try { + const auto deadlineTime = std::chrono::system_clock::time_point( + std::chrono::seconds(deadline.tv_sec) + std::chrono::nanoseconds(deadline.tv_nsec)); + timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); + } catch (const std::system_error&) { + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + throw; + } + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + return timeout ? threadTimedout : threadSuccess; +} + int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); return threadSuccess; @@ -229,7 +273,7 @@ int SignalCondition_nid_no_patch(void** handle) { int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target); + const auto deadline = XtimeDeadline(*target, "_Thrd_sleep"); for (;;) { timespec now{}; if (clock_gettime(CLOCK_REALTIME, &now) != 0) @@ -250,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 7bc8c87c19..056d0ab413 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #ifdef _WIN32 @@ -19,9 +22,30 @@ namespace { constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; -constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147483648\n"); +constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147481748\n"); constexpr std::int64_t SECONDS_PER_DAY = 86400; constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306; +#ifdef _WIN32 +constexpr std::int64_t CALENDAR_CYCLE_YEARS = 400; +constexpr std::int64_t CALENDAR_CYCLE_SECONDS = 146097 * SECONDS_PER_DAY; +constexpr std::int64_t HOST_LAST_SECOND = 32535215999; + +std::int64_t calendarCycles(std::int64_t value, std::int64_t cycle) { + return value >= 0 ? value / cycle : -((-(value + 1)) / cycle + 1); +} + +std::int64_t utcSeconds(const std::tm& time) { + const std::int64_t years = calendarCycles(time.tm_mon, 12); + const std::int64_t month = time.tm_mon - years * 12 + 1; + const std::int64_t year = std::int64_t{time.tm_year} + 1900 + years - (month <= 2 ? 1 : 0); + const std::int64_t era = calendarCycles(year, CALENDAR_CYCLE_YEARS); + const std::int64_t yearOfEra = year - era * CALENDAR_CYCLE_YEARS; + const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5; + const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + const std::int64_t days = era * 146097 + dayOfEra - 719468 + time.tm_mday - 1; + return days * SECONDS_PER_DAY + std::int64_t{time.tm_hour} * 3600 + std::int64_t{time.tm_min} * 60 + time.tm_sec; +} +#endif void copyCalendarFields(const std::tm& host, GuestTm& guest) { guest.tm_sec = host.tm_sec; @@ -104,53 +128,68 @@ const bool timeZoneFixed = [] { } // namespace #endif +extern "C" int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr); + namespace { -bool isFreeBsdConversion(char specifier) { - return specifier == 'k' || specifier == 'l' || specifier == 's' || specifier == 'v' || specifier == '+'; +constexpr const char* WEEKDAY_FULL_NAMES[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; +constexpr const char* MONTH_FULL_NAMES[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", + "October", "November", "December"}; +constexpr int YEAR_BASE = 1900; + +enum class Padding { Default, Less, Space, Zero }; + +const char* paddingFormat(int width, Padding padding, bool spaced) { + static constexpr const char* formats[][4] = { + {"%02d", "%d", "%2d", "%02d"}, + {"%2d", "%d", "%2d", "%02d"}, + {"%03d", "%d", "%3d", "%03d"}, + {"%04d", "%d", "%4d", "%04d"}, + }; + const int row = width == 2 ? (spaced ? 1 : 0) : width == 3 ? 2 : 3; + return formats[row][static_cast(padding)]; } -struct FormatConversion { - std::size_t length; - char specifier; - bool literal; - bool padded; +bool isLeapYearSum(int year, int base) { + return isLeapYear(year % 400 + base % 400); +} + +struct FormatBuffer { + char* cursor; + char* limit; }; -FormatConversion parseConversion(const char* afterPercent) { - const char* cursor = afterPercent; - bool alternative = false; - bool padded = false; - while (*cursor != '\0') { - if (*cursor == 'E' || *cursor == 'O') { - if (alternative) break; - alternative = true; - } else if (*cursor == '-' || *cursor == '_' || *cursor == '0') { - if (padded) break; - padded = true; - } else { - break; - } - ++cursor; - } - if (*cursor == '\0') - return {static_cast(cursor - afterPercent), cursor[-1], true, padded}; - const bool literal = *cursor == '%' || std::strchr("aAbBcCdDeFgGhHIjklmMnprRsStTuUVvWwXxyYzZ+", *cursor) == nullptr; - return {static_cast(cursor - afterPercent) + 1, *cursor, literal, padded}; +void addText(FormatBuffer& buffer, const char* text) { + while (buffer.cursor < buffer.limit && (*buffer.cursor = *text++) != '\0') ++buffer.cursor; } -bool requiresFreeBsdExpansion(const char* format) { - while (*format != '\0') { - if (*format++ != '%') continue; - const auto conversion = parseConversion(format); - if (conversion.literal || (!conversion.padded && isFreeBsdConversion(conversion.specifier))) return true; - format += conversion.length; +void addNumber(FormatBuffer& buffer, int value, const char* format) { + char text[16]; + std::snprintf(text, sizeof(text), format, value); + addText(buffer, text); +} + +void addYear(FormatBuffer& buffer, int year, int base, bool century, bool lastDigits) { + int trail = year % 100 + base % 100; + int lead = year / 100 + base / 100 + trail / 100; + trail %= 100; + if (trail < 0 && lead > 0) { + trail += 100; + --lead; + } else if (lead < 0 && trail > 0) { + trail -= 100; + ++lead; } - return false; + if (century) { + if (lead == 0 && trail < 0) addText(buffer, "-0"); + else addNumber(buffer, lead, "%02d"); + } + if (lastDigits) addNumber(buffer, trail < 0 ? -trail : trail, "%02d"); } const char* guestZoneName(const GuestTm& guest) { if (guest.tm_zone != nullptr) return guest.tm_zone; + if (guest.tm_isdst < 0) return ""; #ifdef _WIN32 return _tzname[guest.tm_isdst != 0 ? 1 : 0]; #else @@ -158,65 +197,220 @@ const char* guestZoneName(const GuestTm& guest) { #endif } -std::string expandFreeBsdConversions(const char* format, const GuestTm& guest) { - std::string result; - while (*format != '\0') { - const char next = *format++; - if (next != '%') { - result += next; - continue; - } - const auto conversion = parseConversion(format); - if (conversion.literal) { - if (conversion.specifier == '%') result += '%'; - result += conversion.specifier; - format += conversion.length; - continue; - } - if (conversion.padded || !isFreeBsdConversion(conversion.specifier)) { - result.append(format - 1, conversion.length + 1); - format += conversion.length; - continue; - } - format += conversion.length; - char text[32]; - switch (conversion.specifier) { - case 'k': - std::snprintf(text, sizeof(text), "%2d", guest.tm_hour); - result += text; - break; - case 'l': - std::snprintf(text, sizeof(text), "%2d", guest.tm_hour % 12 == 0 ? 12 : guest.tm_hour % 12); - result += text; - break; - case 's': { - std::tm host = toHostTm(guest); - std::snprintf(text, sizeof(text), "%lld", static_cast(std::mktime(&host))); - result += text; +void addIsoWeek(FormatBuffer& buffer, char conversion, Padding padding, const GuestTm& time) { + int base = YEAR_BASE; + int yday = time.tm_yday; + int week = 0; + for (;;) { + const int length = isLeapYearSum(time.tm_year, base) ? 366 : 365; + const int bottom = (yday + 11 - time.tm_wday) % 7 - 3; + int top = bottom - length % 7; + if (top < -3) top += 7; + top += length; + if (yday >= top) { + ++base; + week = 1; break; } - case 'v': - result += "%e-%b-%Y"; - break; - default: - result += "%a %b %e %H:%M:%S "; - for (const char* zone = guestZoneName(guest); *zone != '\0'; ++zone) { - if (*zone == '%') result += '%'; - result += *zone; - } - result += " %Y"; + if (yday >= bottom) { + week = 1 + (yday - bottom) / 7; break; } + --base; + yday += isLeapYearSum(time.tm_year, base) ? 366 : 365; } - return result; + if (conversion == 'V') addNumber(buffer, week, paddingFormat(2, padding, false)); + else addYear(buffer, time.tm_year, base, conversion == 'G', true); } +void formatTime(FormatBuffer& buffer, const char* format, const GuestTm& time) { + for (; *format != '\0'; ++format) { + if (*format == '%') { + bool alternative = false; + Padding padding = Padding::Default; + bool handled = true; + for (bool modifier = true; modifier;) { + modifier = false; + switch (*++format) { + case '\0': + --format; + handled = false; + break; + case 'A': + addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_FULL_NAMES[time.tm_wday]); + break; + case 'a': + addText(buffer, time.tm_wday < 0 || time.tm_wday >= 7 ? "?" : WEEKDAY_NAMES[time.tm_wday]); + break; + case 'B': + addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_FULL_NAMES[time.tm_mon]); + break; + case 'b': + case 'h': + addText(buffer, time.tm_mon < 0 || time.tm_mon >= 12 ? "?" : MONTH_NAMES[time.tm_mon]); + break; + case 'C': + addYear(buffer, time.tm_year, YEAR_BASE, true, false); + break; + case 'c': + formatTime(buffer, "%a %b %e %H:%M:%S %Y", time); + break; + case 'D': + formatTime(buffer, "%m/%d/%y", time); + break; + case 'd': + addNumber(buffer, time.tm_mday, paddingFormat(2, padding, false)); + break; + case 'E': + case 'O': + if (alternative) { + handled = false; + break; + } + alternative = true; + modifier = true; + break; + case 'e': + addNumber(buffer, time.tm_mday, paddingFormat(2, padding, true)); + break; + case 'F': + formatTime(buffer, "%Y-%m-%d", time); + break; + case 'H': + addNumber(buffer, time.tm_hour, paddingFormat(2, padding, false)); + break; + case 'I': + addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, false)); + break; + case 'j': + addNumber(buffer, time.tm_yday + 1, paddingFormat(3, padding, false)); + break; + case 'k': + addNumber(buffer, time.tm_hour, paddingFormat(2, padding, true)); + break; + case 'l': + addNumber(buffer, time.tm_hour % 12 != 0 ? time.tm_hour % 12 : 12, paddingFormat(2, padding, true)); + break; + case 'M': + addNumber(buffer, time.tm_min, paddingFormat(2, padding, false)); + break; + case 'm': + addNumber(buffer, time.tm_mon + 1, paddingFormat(2, padding, false)); + break; + case 'n': + addText(buffer, "\n"); + break; + case 'p': + addText(buffer, time.tm_hour >= 12 ? "PM" : "AM"); + break; + case 'R': + formatTime(buffer, "%H:%M", time); + break; + case 'r': + formatTime(buffer, "%I:%M:%S %p", time); + break; + case 'S': + addNumber(buffer, time.tm_sec, paddingFormat(2, padding, false)); + break; + case 's': { + GuestTm copy = time; + char text[24]; + std::snprintf(text, sizeof(text), "%lld", static_cast(libc_mktime_nid_postfix(©))); + addText(buffer, text); + break; + } + case 'T': + formatTime(buffer, "%H:%M:%S", time); + break; + case 't': + addText(buffer, "\t"); + break; + case 'U': + addNumber(buffer, (time.tm_yday + 7 - time.tm_wday) / 7, paddingFormat(2, padding, false)); + break; + case 'u': + addNumber(buffer, time.tm_wday == 0 ? 7 : time.tm_wday, "%d"); + break; + case 'V': + case 'G': + case 'g': + addIsoWeek(buffer, *format, padding, time); + break; + case 'v': + formatTime(buffer, "%e-%b-%Y", time); + break; + case 'W': + addNumber(buffer, (time.tm_yday + 7 - (time.tm_wday != 0 ? time.tm_wday - 1 : 6)) / 7, + paddingFormat(2, padding, false)); + break; + case 'w': + addNumber(buffer, time.tm_wday, "%d"); + break; + case 'X': + formatTime(buffer, "%H:%M:%S", time); + break; + case 'x': + formatTime(buffer, "%m/%d/%y", time); + break; + case 'y': + addYear(buffer, time.tm_year, YEAR_BASE, false, true); + break; + case 'Y': + addYear(buffer, time.tm_year, YEAR_BASE, true, true); + break; + case 'Z': + addText(buffer, guestZoneName(time)); + break; + case 'z': { + if (time.tm_isdst < 0) break; + int difference = static_cast(time.tm_gmtoff); + addText(buffer, difference < 0 ? "-" : "+"); + if (difference < 0) difference = -difference; + difference /= 60; + addNumber(buffer, difference / 60 * 100 + difference % 60, paddingFormat(4, padding, false)); + break; + } + case '+': + formatTime(buffer, "%a %b %e %H:%M:%S %Z %Y", time); + break; + case '-': + case '_': + case '0': + if (padding != Padding::Default) { + handled = false; + break; + } + padding = *format == '-' ? Padding::Less : *format == '_' ? Padding::Space : Padding::Zero; + modifier = true; + break; + default: + handled = false; + break; + } + } + if (handled) continue; + } + if (buffer.cursor == buffer.limit) break; + *buffer.cursor++ = *format; + } +} + +std::size_t formatGuestTime(char* destination, std::size_t size, const char* format, const GuestTm& time) { + FormatBuffer buffer{destination, destination + size}; + formatTime(buffer, format == nullptr ? "%c" : format, time); + if (buffer.cursor == buffer.limit) return 0; + *buffer.cursor = '\0'; + return static_cast(buffer.cursor - destination); +} } extern "C" { 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); +std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state); +int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { #ifdef _WIN32 @@ -274,10 +468,21 @@ GuestTm* APS5_VABI localtime_nid_postfix(const int64_t* timer) { GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result) { if (timer == nullptr || result == nullptr) return nullptr; - const std::time_t t = static_cast(*timer); std::tm host{}; + const int callerError = errno; #ifdef _WIN32 - if (localtime_s(&host, &t) != 0) return nullptr; + const std::int64_t cycles = *timer >= 0 && *timer <= HOST_LAST_SECOND ? 0 : calendarCycles(*timer, CALENDAR_CYCLE_SECONDS); + const std::time_t t = static_cast(*timer - cycles * CALENDAR_CYCLE_SECONDS); + if (localtime_s(&host, &t) != 0) { + errno = callerError; + return nullptr; + } + const std::int64_t year = host.tm_year + cycles * CALENDAR_CYCLE_YEARS; + if (year < std::numeric_limits::min() || year > std::numeric_limits::max()) { + errno = callerError; + return nullptr; + } + host.tm_year = static_cast(year); copyCalendarFields(host, *result); long standardBias = 0; long daylightBias = 0; @@ -287,7 +492,11 @@ GuestTm* APS5_VABI localtime_s_nid_postfix(const int64_t* timer, GuestTm* result result->tm_gmtoff = -static_cast(standardBias + (daylight ? daylightBias : 0)); result->tm_zone = _tzname[daylight ? 1 : 0]; #else - if (localtime_r(&t, &host) == nullptr) return nullptr; + const std::time_t t = static_cast(*timer); + if (localtime_r(&t, &host) == nullptr) { + errno = callerError; + return nullptr; + } copyCalendarFields(host, *result); result->tm_gmtoff = host.tm_gmtoff; result->tm_zone = host.tm_zone; @@ -310,10 +519,26 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) { int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) { std::tm host = toHostTm(*timeptr); host.tm_wday = -1; + int64_t moved = 0; +#ifdef _WIN32 + const bool ordinary = host.tm_year > 70 && host.tm_year < 1100 && host.tm_mon >= 0 && host.tm_mon < 12 && host.tm_mday >= 1 && host.tm_mday <= 31 + && host.tm_hour >= 0 && host.tm_hour < 24 && host.tm_min >= 0 && host.tm_min < 60 && host.tm_sec >= 0 && host.tm_sec <= 60; + const std::int64_t approximate = ordinary ? SECONDS_PER_DAY : utcSeconds(host); + if (approximate < SECONDS_PER_DAY || approximate > HOST_LAST_SECOND - SECONDS_PER_DAY) { + moved = calendarCycles(approximate - SECONDS_PER_DAY, CALENDAR_CYCLE_SECONDS) * CALENDAR_CYCLE_SECONDS; + GuestTm window{}; + if (!utcCalendarFields(approximate - moved, window)) return -1; + window.tm_isdst = host.tm_isdst; + host = toHostTm(window); + host.tm_wday = -1; + } +#endif const std::time_t t = std::mktime(&host); if (t == static_cast(-1) && host.tm_wday == -1) return -1; - const int64_t time = static_cast(t); - localtime_s_nid_postfix(&time, timeptr); + const int64_t time = static_cast(t) + moved; + GuestTm normalized{}; + if (localtime_s_nid_postfix(&time, &normalized) == nullptr) return -1; + *timeptr = normalized; return time; } @@ -322,18 +547,23 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) { } size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) { - const std::tm host = toHostTm(*timeptr); - if (!requiresFreeBsdExpansion(format)) return std::strftime(str, count, format, &host); - const std::string expanded = expandFreeBsdConversions(format, *timeptr); - return std::strftime(str, count, expanded.c_str(), &host); + tzset_nid_postfix(); + return formatGuestTime(str, count, format, *timeptr); } char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { static thread_local char result[ASCTIME_SIZE]; + if (timeptr == nullptr) { + errno = EINVAL; + std::memcpy(result, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); + return result; + } const char* weekday = timeptr->tm_wday >= 0 && timeptr->tm_wday < 7 ? WEEKDAY_NAMES[timeptr->tm_wday] : "???"; const char* month = timeptr->tm_mon >= 0 && timeptr->tm_mon < 12 ? MONTH_NAMES[timeptr->tm_mon] : "???"; - std::snprintf(result, sizeof(result), "%s %s%3d %.2d:%.2d:%.2d %lld\n", weekday, month, timeptr->tm_mday, - timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, 1900LL + timeptr->tm_year); + char year[16]; + formatGuestTime(year, sizeof(year), "%Y", *timeptr); + std::snprintf(result, sizeof(result), std::strlen(year) <= 4 ? "%s %s%3d %.2d:%.2d:%.2d %-4s\n" : "%s %s%3d %.2d:%.2d:%.2d %s\n", + weekday, month, timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, year); return result; } @@ -349,6 +579,42 @@ size_t APS5_VABI strftime_nid_postfix(char* str, size_t count, const char* forma return libc_strftime_nid_postfix(str, count, format, timeptr); } +size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const std::uint16_t* format, const GuestTm* timeptr) { + constexpr int GuestEnomem = 12; + constexpr int GuestEinval = 22; + constexpr auto Failed = static_cast(-1); + if (format == nullptr) throw std::invalid_argument("wcsftime: null format"); + if (timeptr == nullptr) throw std::invalid_argument("wcsftime: null time"); + const std::uint16_t* formatCursor = format; + const std::size_t formatLength = wcsrtombs_nid_postfix(nullptr, &formatCursor, 0, nullptr); + if (formatLength == Failed) return 0; + const std::unique_ptr narrowFormat(new (std::nothrow) char[formatLength + 1]); + if (!narrowFormat) { + errno = GuestEnomem; + return 0; + } + wcsrtombs_nid_postfix(narrowFormat.get(), &formatCursor, formatLength + 1, nullptr); + const auto maxCharacterBytes = static_cast(___mb_cur_max_nid_postfix()); + if (std::numeric_limits::max() / maxCharacterBytes <= count) { + errno = GuestEinval; + return 0; + } + const std::unique_ptr narrow(new (std::nothrow) char[count * maxCharacterBytes]); + if (!narrow) { + errno = GuestEnomem; + return 0; + } + const int callerError = errno; + if (strftime_nid_postfix(narrow.get(), count, narrowFormat.get(), timeptr) == 0) { + errno = callerError; + return 0; + } + const char* narrowCursor = narrow.get(); + const std::size_t length = mbsrtowcs_nid_postfix(str, &narrowCursor, count, nullptr); + if (length == static_cast(-2) || length == Failed || narrowCursor != nullptr) return 0; + return length; +} + // The guest's CLOCKS_PER_SEC is 1000000: clock() reports process CPU time in microseconds. int64_t APS5_VABI clock_nid_postfix() { #ifdef _WIN32 diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 9725d71255..0ccea4f8ad 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -6,6 +6,9 @@ #include #ifdef _WIN32 #include +#else +#include +#include #endif namespace LibcException { @@ -23,7 +26,15 @@ static void RawWrite(const char* data, std::size_t size) { size -= written; } #else - std::fwrite(data, 1, size, stderr); + const int kept = HostStandardError_nid_no_patch(); + const int descriptor = kept >= 0 ? kept : STDERR_FILENO; + while (size != 0) { + const auto written = ::write(descriptor, data, size); + if (written < 0 && errno == EINTR) continue; + if (written <= 0) return; + data += written; + size -= static_cast(written); + } #endif } @@ -263,7 +274,7 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { ThrowFromSite(object, type, destructor, __builtin_return_address(0)); } -LibcException::Header* __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { using namespace LibcException; auto* header = FromObject(object); AllocationOf(header)->references.store(0, std::memory_order_relaxed); @@ -385,8 +396,8 @@ void APS5_VABI __cxa_rethrow_primary_exception_nid_postfix(void* object) { InvokeTerminate(header->terminate); } -void* __dynamic_cast_nid_postfix(const void* source, const __cxxabiv1::__class_type_info* sourceType, - const __cxxabiv1::__class_type_info* destinationType, std::ptrdiff_t) { +void* APS5_VABI __dynamic_cast_nid_postfix(const void* source, const __cxxabiv1::__class_type_info* sourceType, + const __cxxabiv1::__class_type_info* destinationType, std::ptrdiff_t) { using namespace LibcException; if (!source) return nullptr; auto* vtable = *static_cast(source); diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 9e425979fa..fdaca18500 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -1,7 +1,13 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include #include #include #include +#include + +extern "C" void* APS5_VABI _Znwm_nid_postfix(std::size_t size); namespace LibcException { struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; @@ -119,9 +125,143 @@ ExceptionObject* Assign(ExceptionObject* object, const ExceptionObject* source) object->vtable = &table.destroy; __cxa_throw_nid_postfix(object, reinterpret_cast(const_cast(table.type)), [](void* p) { DestroyPlain(static_cast(p)); }); } + +struct CategoryObject { const void* vtable; }; +struct ErrorCode { int value; const CategoryObject* category; }; +struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; +static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); +struct CppError { int value; const char* message; }; + +static const char* GenericErrorMessage(int value) { + static constexpr CppError errors[] { + {1, "operation not permitted"}, + {2, "no such file or directory"}, + {3, "no such process"}, + {4, "interrupted"}, + {5, "io error"}, + {6, "no such device or address"}, + {7, "argument list too long"}, + {8, "executable format error"}, + {9, "bad file descriptor"}, + {10, "no child process"}, + {11, "resource deadlock would occur"}, + {12, "not enough memory"}, + {13, "permission denied"}, + {14, "bad address"}, + {16, "device or resource busy"}, + {17, "file exists"}, + {18, "cross device link"}, + {19, "no such device"}, + {20, "not a directory"}, + {21, "is a directory"}, + {22, "invalid argument"}, + {23, "too many files open in system"}, + {24, "too many files open"}, + {25, "inappropriate io control operation"}, + {26, "text file busy"}, + {27, "file too large"}, + {28, "no space on device"}, + {29, "invalid seek"}, + {30, "read only file system"}, + {31, "too many links"}, + {32, "broken pipe"}, + {33, "argument out of domain"}, + {34, "result out of range"}, + {35, "operation would block"}, + {36, "operation in progress"}, + {37, "connection already in progress"}, + {38, "not a socket"}, + {39, "destination address required"}, + {40, "message size"}, + {41, "wrong protocol type"}, + {42, "no protocol option"}, + {43, "protocol not supported"}, + {45, "not supported"}, + {47, "address family not supported"}, + {48, "address in use"}, + {49, "address not available"}, + {50, "network down"}, + {51, "network unreachable"}, + {52, "network reset"}, + {53, "connection aborted"}, + {54, "connection reset"}, + {55, "no buffer space"}, + {56, "already connected"}, + {57, "not connected"}, + {60, "timed out"}, + {61, "connection refused"}, + {62, "too many symbolic link levels"}, + {63, "filename too long"}, + {65, "host unreachable"}, + {66, "directory not empty"}, + {77, "no lock available"}, + {78, "function not supported"}, + {82, "identifier removed"}, + {83, "no message"}, + {84, "value too large"}, + {85, "operation canceled"}, + {86, "illegal byte sequence"}, + {89, "bad message"}, + {91, "no link"}, + {92, "protocol error"}, + {95, "state not recoverable"}, + {96, "owner dead"} + }; + for (const auto& error : errors) + if (error.value == value) return error.message; + return nullptr; +} + +const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr}; +const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType}; +static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {} +static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) { + throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated"); +} +static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } +static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + if (!result) throw std::invalid_argument("std::_Generic_error_category::message: null result"); + const char* message = GenericErrorMessage(value); + if (!message) message = "unknown error"; + const auto length = std::strlen(message); + const auto capacity = length | std::size_t{15}; + char* storage = length <= 15 ? result->buffer : static_cast(_Znwm_nid_postfix(capacity + 1)); + std::memcpy(storage, message, length + 1); + result->reserved = 0; + if (length > 15) result->pointer = storage; + result->size = length; + result->capacity = capacity; + return result; +} +static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; } +static bool APS5_VABI GenericCategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { + return condition->category == self && condition->value == value; +} +static bool APS5_VABI GenericCategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { + return code->category == self && code->value == value; +} +const void* const GenericCategoryVtable[] { + nullptr, &GenericCategoryType, + reinterpret_cast(GenericCategoryDestroy_nid_no_patch), reinterpret_cast(GenericCategoryDelete_nid_no_patch), + reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(GenericCategoryMessage_nid_no_patch), + reinterpret_cast(GenericCategoryCondition_nid_no_patch), + reinterpret_cast(GenericCategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(GenericCategoryEquivalentCode_nid_no_patch) +}; +const CategoryObject GenericCategory {GenericCategoryVtable + 2}; } extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) { + return LibcException::GenericErrorMessage(value); +} +const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { + return &LibcException::GenericCategory; +} +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const LibcException::CategoryObject* self, int value) { + return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value); +} LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr}; LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr}; LibcException::ExceptionVtable _ZTVSt9exception_nid_postfix {0, &_ZTISt9exception_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; @@ -271,9 +411,19 @@ void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(LibcException::ExceptionObjec void APS5_VABI _ZNSt12system_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DestroyMessage(self); } void APS5_VABI _ZNSt12system_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self) { LibcException::DeleteMessage(self); } const char* APS5_VABI _ZNKSt12system_error4whatEv_nid_postfix(const LibcException::ExceptionObject* self) { return LibcException::MessageWhat(self); } -void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt12system_error_nid_postfix); } -void APS5_VABI _ZNSt12system_errorC2ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt12system_error_nid_postfix); } -LibcException::ExceptionObject* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } +void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Copy(&self->base, &source->base, _ZTVSt12system_error_nid_postfix); + self->code = source->code; +} +void APS5_VABI _ZNSt12system_errorC2ERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Copy(&self->base, &source->base, _ZTVSt12system_error_nid_postfix); + self->code = source->code; +} +LibcException::SystemErrorObject* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(LibcException::SystemErrorObject* self, const LibcException::SystemErrorObject* source) { + LibcException::Assign(&self->base, &source->base); + self->code = source->code; + return self; +} LibcException::TypeRecord _ZTISt11regex_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St11regex_error", &_ZTISt13runtime_error_nid_postfix}; LibcException::ExceptionVtable _ZTVSt11regex_error_nid_postfix {0, &_ZTISt11regex_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; @@ -321,4 +471,25 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt11regex_error_nid_postfix), reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); } +[[noreturn]] void ThrowCppError_nid_no_patch(int code) { + static constexpr LibcException::CppError errors[] { + {16, "device or resource busy"}, + {22, "invalid argument"}, + {3, "no such process"}, + {12, "not enough memory"}, + {1, "operation not permitted"}, + {11, "resource deadlock would occur"}, + {35, "resource unavailable try again"} + }; + if (code < 0 || code >= static_cast(std::size(errors))) + throw std::out_of_range("_Throw_Cpp_error: " + std::to_string(code) + " is not a _Cpp_error code"); + const auto& error = errors[code]; + char message[128]; + std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value)); + auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject))); + LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message); + object->code = {error.value, &LibcException::GenericCategory}; + __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12system_error_nid_postfix), + reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); +} } 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/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index a05a32369a..6aac4ddddb 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -21,6 +21,73 @@ #include #endif +namespace { +constexpr int CursorInstructionRegister = -1; +constexpr int CursorStackRegister = -2; +constexpr int CursorRegisterCount = 17; + +std::uintptr_t* CursorRegister(_Unwind_Context* context, int number) { + if (number == CursorInstructionRegister) return &context->registers[16]; + if (number == CursorStackRegister) return &context->registers[7]; + if (number >= 0 && number < CursorRegisterCount) return &context->registers[number]; + return nullptr; +} + +bool APS5_VABI CursorValidRegister(_Unwind_Context* context, int number) { + return CursorRegister(context, number) != nullptr; +} + +std::uintptr_t APS5_VABI CursorGetRegister(_Unwind_Context* context, int number) { + auto* value = CursorRegister(context, number); + if (!value) NotImplemented_nid_no_patch("libunwind cursor: read of an unknown register"); + return value ? *value : 0; +} + +void APS5_VABI CursorSetRegister(_Unwind_Context* context, int number, std::uintptr_t value) { + auto* slot = CursorRegister(context, number); + if (!slot) NotImplemented_nid_no_patch("libunwind cursor: write of an unknown register"); + else *slot = value; +} + +bool APS5_VABI CursorValidFloatRegister(_Unwind_Context*, int) { + return false; +} + +bool APS5_VABI CursorIsSignalFrame(_Unwind_Context* context) { + return context->signalFrame; +} + +void APS5_VABI CursorUnsupported(_Unwind_Context*) { + NotImplemented_nid_no_patch("libunwind cursor: operation other than register access or isSignalFrame"); +} + +template std::uintptr_t Slot(Function function) { + return reinterpret_cast(function); +} + +const std::uintptr_t CursorSlots[] = { + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorValidRegister), + Slot(&CursorGetRegister), + Slot(&CursorSetRegister), + Slot(&CursorValidFloatRegister), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorIsSignalFrame), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), + Slot(&CursorUnsupported), +}; +} + +const std::uintptr_t* UnwindCursorSlots_nid_no_patch() { + return CursorSlots; +} + #if defined(__linux__) || defined(_WIN32) || defined(__APPLE__) extern "C" _Unwind_Reason_Code __gxx_personality_v0(int, _Unwind_Action, std::uint64_t, _Unwind_Exception*, _Unwind_Context*); @@ -42,8 +109,6 @@ _Unwind_Reason_Code CallPersonality(Word personality, _Unwind_Action actions, _U return __gcc_personality_v0_nid_postfix(1, actions, exception->exception_class, exception, context); return (actions & _UA_SEARCH_PHASE) ? _URC_FATAL_PHASE1_ERROR : _URC_FATAL_PHASE2_ERROR; } -struct Lookup { Word pc; const Byte* fde {}; Word text {}; Word data {}; }; - #if defined(__linux__) || defined(__APPLE__) int FindFrame(dl_phdr_info* info, std::size_t, void* argument) { auto& query = *static_cast(argument); @@ -80,70 +145,6 @@ int FindFrame(dl_phdr_info* info, std::size_t, void* argument) { #endif -struct Frame { - const Byte* cieBegin {}; - const Byte* cieEnd {}; - const Byte* begin {}; - const Byte* end {}; - Word start {}, length {}, personality {}, lsda {}; - Word codeAlign {}; - std::intptr_t dataAlign {}; - unsigned returnRegister {}; - bool signal {}; -#ifdef _WIN32 - bool foreign {}; - _Unwind_Context caller {}; -#endif -}; - -bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) { - if (!query.fde) return false; - p = query.fde; - auto length = Read(p); - if (!length || length == 0xffffffff) return false; - frame.end = p + length; - const Byte* ciePointer = p; - auto cieOffset = Read(p); - const Byte* cie = ciePointer - cieOffset; - const Byte* c = cie; - auto cieLength = Read(c); - frame.cieEnd = c + cieLength; - if (Read(c) != 0) return false; - Byte version = *c++; - if (version != 1 && version != 3 && version != 4) return false; - augmentation = reinterpret_cast(c); - while (*c++) {} - if (version == 4 && (*c++ != sizeof(Word) || *c++ != 0)) return false; - frame.codeAlign = Uleb(c); - frame.dataAlign = Sleb(c); - frame.returnRegister = version == 1 ? *c++ : Uleb(c); -#ifdef _WIN32 - if (frame.returnRegister >= 17 && frame.returnRegister != 32) return false; -#else - if (frame.returnRegister >= 17) return false; -#endif - Byte pointerEncoding = 0; - lsdaEncoding = 255; - if (*augmentation == 'z') { - Word size = Uleb(c); - const Byte* end = c + size; - for (const char* a = augmentation + 1; *a; ++a) { - switch (*a) { - case 'R': pointerEncoding = *c++; break; - case 'L': lsdaEncoding = *c++; break; - case 'P': { Byte encoding = *c++; frame.personality = Encoded(c, encoding, query.data, 0, query.text); break; } - case 'S': frame.signal = true; break; - default: return false; - } - } - c = end; - } else if (*augmentation) return false; - frame.cieBegin = c; - frame.start = Encoded(p, pointerEncoding, query.data, 0, query.text); - frame.length = Encoded(p, pointerEncoding & 15); - return true; -} - bool DecodeCandidate(_Unwind_Context& context, Frame& frame, const Lookup& query) { const Byte* p; const char* augmentation; @@ -305,69 +306,6 @@ bool DecodeFrame(_Unwind_Context& context, Frame& frame) { #endif } -struct Rule { unsigned kind {}; std::intptr_t value {}; const Byte* expression {}; }; -#ifdef _WIN32 -constexpr unsigned RuleCount = 33; -#else -constexpr unsigned RuleCount = 17; -#endif - -struct Rules { Rule registers[RuleCount] {}; unsigned cfaRegister {7}; std::intptr_t cfaOffset {}; const Byte* cfaExpression {}; }; - -bool Instructions(const Byte* p, const Byte* end, const Frame& frame, Word target, Rules& state, const Rules& initial) { - Word location = frame.start; - Rules saved[16]; - unsigned depth = 0; - while (p < end && location <= target) { - Byte opcode = *p++; - unsigned reg; - if ((opcode & 192) == 64) { location += (opcode & 63) * frame.codeAlign; continue; } - if ((opcode & 192) == 128) { - reg = opcode & 63; - auto offset = Uleb(p) * frame.dataAlign; - if (reg < RuleCount) state.registers[reg] = {1, static_cast(offset)}; - continue; - } - if ((opcode & 192) == 192) { - reg = opcode & 63; - if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; - continue; - } - switch (opcode) { - case 0: break; - case 1: location = Read(p); break; - case 2: location += Read(p) * frame.codeAlign; break; - case 3: location += Read(p) * frame.codeAlign; break; - case 4: location += Read(p) * frame.codeAlign; break; - case 5: case 17: case 20: case 21: { - reg = Uleb(p); - auto offset = (opcode == 17 || opcode == 21 ? Sleb(p) : std::intptr_t(Uleb(p))) * frame.dataAlign; - if (reg < RuleCount) state.registers[reg] = {unsigned(opcode >= 20 ? 4 : 1), offset}; - break; - } - case 6: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = initial.registers[reg]; break; - case 7: case 8: reg = Uleb(p); if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 7 ? 5 : 0)}; break; - case 9: { reg = Uleb(p); auto other = Uleb(p); if (reg < RuleCount) state.registers[reg] = {2, std::intptr_t(other)}; break; } - case 10: if (depth == 16) return false; saved[depth++] = state; break; - case 11: if (!depth) return false; state = saved[--depth]; break; - case 12: case 18: - state.cfaExpression = nullptr; state.cfaRegister = Uleb(p); - state.cfaOffset = opcode == 18 ? Sleb(p) * frame.dataAlign : Uleb(p); break; - case 13: state.cfaRegister = Uleb(p); break; - case 14: case 19: state.cfaOffset = opcode == 19 ? Sleb(p) * frame.dataAlign : Uleb(p); break; - case 15: { state.cfaExpression = p; auto size = Uleb(p); p += size; break; } - case 16: case 22: { - reg = Uleb(p); const Byte* expr = p; auto size = Uleb(p); p += size; - if (reg < RuleCount) state.registers[reg] = {unsigned(opcode == 16 ? 3 : 6), 0, expr}; - break; - } - case 46: Uleb(p); break; - default: return false; - } - } - return true; -} - bool Expression(const Byte* p, const _Unwind_Context& context, Word cfa, Word& result) { Word length = Uleb(p); const Byte* end = p + length; diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp new file mode 100644 index 0000000000..7e30971457 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp @@ -0,0 +1,202 @@ +#ifdef _WIN32 +#include +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void CallTraceEnter_nid_no_patch(); + +namespace { + +struct ImportRecord { + void* target; + std::string name; + std::string library; + mutable std::atomic calls{0}; +}; + +struct RingSlot { + std::atomic sequence{0}; + std::atomic thread{0}; + std::atomic returnAddress{0}; + std::atomic record{nullptr}; +}; + +constexpr std::size_t RingCapacity = 65536; +constexpr std::size_t ThunkBytes = 24; +constexpr std::size_t ThunkAreaBytes = 1024 * 1024; + +std::mutex g_mutex; +std::deque g_records; +std::unordered_map g_thunkByTarget; +std::unique_ptr g_ring; +std::atomic g_sequence{0}; +std::uint8_t* g_writable = nullptr; +std::uint8_t* g_executable = nullptr; +std::size_t g_thunks = 0; + +bool Selected(std::string_view library) { + const char* value = std::getenv("APS5_TRACE_CALLS"); + if (value == nullptr || *value == '\0') return false; + std::string_view rest(value); + if (library.ends_with(".prx")) library.remove_suffix(4); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.ends_with(".prx")) token.remove_suffix(4); + if (token == "all" || token == library) return true; + } + return false; +} + +void MapThunkArea() { + const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); + if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); + g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); + g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); + const DWORD code = GetLastError(); + CloseHandle(section); + if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); +} + +void* WriteThunk(const ImportRecord& record) { + if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); + const std::size_t offset = g_thunks * ThunkBytes; + const std::uint8_t prefix[] = {0x49, 0xbb}; + const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; + const auto recordAddress = reinterpret_cast(&record); + const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); + std::uint8_t* thunk = g_writable + offset; + std::memcpy(thunk, prefix, sizeof(prefix)); + std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); + std::memcpy(thunk + 10, jump, sizeof(jump)); + std::memcpy(thunk + 16, &handler, sizeof(handler)); + FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); + ++g_thunks; + return g_executable + offset; +} + +} + +static_assert(offsetof(ImportRecord, target) == 0); + +extern "C" { + +void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { + static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); + const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; + RingSlot& slot = g_ring[sequence % RingCapacity]; + slot.sequence.store(0, std::memory_order_release); + slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); + slot.returnAddress.store(returnAddress, std::memory_order_relaxed); + slot.record.store(static_cast(record), std::memory_order_relaxed); + slot.sequence.store(sequence, std::memory_order_release); +} + +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { + if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + std::lock_guard lock(g_mutex); + if (g_ring == nullptr) { + MapThunkArea(); + g_ring = std::make_unique(RingCapacity); + } + if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; + g_records.emplace_back(target, name, library); + void* thunk = WriteThunk(g_records.back()); + g_thunkByTarget.emplace(target, thunk); + return thunk; +} + +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { + if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min({last, capacity, RingCapacity}); + std::size_t count = 0; + for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { + const RingSlot& slot = g_ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + return count; +} + +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { + if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0) return 0; + std::unique_lock lock(g_mutex, std::try_to_lock); + if (!lock.owns_lock()) return 0; + std::size_t count = 0; + for (const auto& record : g_records) { + const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); + if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; + std::size_t position = count < capacity ? count++ : count - 1; + while (position > 0 && entries[position - 1].calls < calls) { + entries[position] = entries[position - 1]; + --position; + } + entries[position] = {calls, record.name.c_str(), record.library.c_str()}; + } + return count; +} + +} + +asm(".text\n" + APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") + " push %rdi\n" + " push %rsi\n" + " push %rdx\n" + " push %rcx\n" + " push %r8\n" + " push %r9\n" + " push %rax\n" + " push %r10\n" + " push %r11\n" + " sub $128, %rsp\n" + " movdqu %xmm0, 0(%rsp)\n" + " movdqu %xmm1, 16(%rsp)\n" + " movdqu %xmm2, 32(%rsp)\n" + " movdqu %xmm3, 48(%rsp)\n" + " movdqu %xmm4, 64(%rsp)\n" + " movdqu %xmm5, 80(%rsp)\n" + " movdqu %xmm6, 96(%rsp)\n" + " movdqu %xmm7, 112(%rsp)\n" + " mov %r11, %rdi\n" + " mov 200(%rsp), %rsi\n" + " call CallTraceRecord_nid_no_patch\n" + " movdqu 0(%rsp), %xmm0\n" + " movdqu 16(%rsp), %xmm1\n" + " movdqu 32(%rsp), %xmm2\n" + " movdqu 48(%rsp), %xmm3\n" + " movdqu 64(%rsp), %xmm4\n" + " movdqu 80(%rsp), %xmm5\n" + " movdqu 96(%rsp), %xmm6\n" + " movdqu 112(%rsp), %xmm7\n" + " add $128, %rsp\n" + " pop %r11\n" + " pop %r10\n" + " pop %rax\n" + " pop %r9\n" + " pop %r8\n" + " pop %rcx\n" + " pop %rdx\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *(%r11)\n"); +#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 53d651430f..a4671a43eb 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,6 +1,7 @@ #ifdef _WIN32 #include #include +#include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/GuestArena.hpp" #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -32,6 +34,7 @@ void Report(const char* format, ...) { void ReportAllThreads(); void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span); +void ReportCallTrace(); void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; @@ -325,12 +328,34 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } } + ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } +void ReportCallTrace() { + static CallTraceEntry entries[64]; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + char line[MAX_PATH + 64]; + Report(" last traced system library calls (APS5_TRACE_CALLS):\n"); + for (std::size_t index = 0; index < count; ++index) { + DescribeAddress(entries[index].returnAddress, line, sizeof(line)); + Report(" #%llu thread %llu %s %s from %s\n", static_cast(entries[index].sequence), + static_cast(entries[index].thread), entries[index].library, entries[index].name, line); + } + static CallTraceCount counts[32]; + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + Report(" most called traced imports (%llu calls in total):\n", static_cast(total)); + for (std::size_t index = 0; index < counted; ++index) { + Report(" %12llu %s %s\n", static_cast(counts[index].calls), counts[index].library, counts[index].name); + } +} + std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) { std::size_t count = 0; std::uint64_t frame = context->Rbp; @@ -451,6 +476,7 @@ void ReportBacktrace(const char* what, bool reportThreads) { Report(" #%u %s\n", static_cast(i), line); } ReportStackCandidates(reinterpret_cast(__builtin_frame_address(0)), 32, 0x4000); + ReportCallTrace(); if (reportThreads) ReportAllThreads(); std::fflush(stderr); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index d18f2e5202..0cceb906ff 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -23,6 +23,7 @@ AppTitleId GetAppTitleId_nid_postfix(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); +std::uint32_t GetAppAttribute3_nid_no_patch(); std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 1737a427dd..be912c6e78 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -9,6 +9,7 @@ struct ParsedParamJson { std::string title; std::string titleId; std::uint64_t downloadDataSizeMiB = 0; + std::uint32_t attribute3 = 0; std::int32_t userDefinedParams[4] = {}; }; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index 79df79827a..e5f172fcea 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -18,6 +18,7 @@ bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; std::uint64_t g_downloadDataSizeMiB = 0; +std::uint32_t g_attribute3 = 0; std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; @@ -38,6 +39,7 @@ void ensureTitleLoaded() { copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; + g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); g_titleLoaded = true; } @@ -54,6 +56,17 @@ const bool g_downloadDataMounted = [] { } }(); +const bool g_dataMounted = [] { + try { + const auto run = std::filesystem::current_path(); + if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false; + std::filesystem::create_directories(run / "data"); + return true; + } catch (const std::exception&) { + return false; + } +}(); + void ensureIconLoaded() { if (g_iconAttempted) return; g_iconAttempted = true; @@ -89,6 +102,11 @@ std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix() { return g_downloadDataSizeMiB; } +std::uint32_t GetAppAttribute3_nid_no_patch() { + ensureTitleLoaded(); + return g_attribute3; +} + std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index) { if (index >= 4) throw std::out_of_range("user defined param index out of range"); ensureTitleLoaded(); diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 927674d8b9..585b732a1d 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -230,7 +230,10 @@ JsonValue parseJsonNumber(const std::string& text, std::size_t& position) { JsonValue value; value.type = JsonType::Number; value.numberText = text.substr(start, position - start); - value.numberValue = std::stod(value.numberText); + 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"); return value; } @@ -305,6 +308,12 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { + const auto& number = attribute->numberText; + if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); + 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* 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 631f5553ee..57199523e2 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -347,15 +347,38 @@ void _addPage(AmmState& state, std::uint64_t address, const AmmPage& page) { if (page.pooled) ++state.pooledUses[page.physical]; } -void _forgetPages(AmmState& state, std::uint64_t address, std::uint64_t size, bool release) { - for (auto it = state.pages.lower_bound(address); it != state.pages.end() && it->first < address + size;) { - if (it->second.pooled) { - const auto uses = state.pooledUses.find(it->second.physical); - if (--uses->second == 0 && release) { - state.pooledUses.erase(uses); - _returnPoolPages(state, it->second.physical, PS5_PAGE_SIZE); - } +void _releasePage(AmmState& state, const AmmPage& page) { + if (!page.pooled) return; + const auto uses = state.pooledUses.find(page.physical); + if (--uses->second == 0) { + state.pooledUses.erase(uses); + _returnPoolPages(state, page.physical, PS5_PAGE_SIZE); + } +} + +class AmmPageReferences { +public: + AmmPageReferences(AmmState& state, const std::vector& pages) : state(state), pages(pages) { + for (const auto& page : pages) { + if (page.pooled) ++state.pooledUses.find(page.physical)->second; } + } + + AmmPageReferences(const AmmPageReferences&) = delete; + AmmPageReferences& operator=(const AmmPageReferences&) = delete; + + ~AmmPageReferences() { + for (const auto& page : pages) _releasePage(state, page); + } + +private: + AmmState& state; + const std::vector& pages; +}; + +void _forgetPages(AmmState& state, std::uint64_t address, std::uint64_t size) { + for (auto it = state.pages.lower_bound(address); it != state.pages.end() && it->first < address + size;) { + _releasePage(state, it->second); it = state.pages.erase(it); } } @@ -385,9 +408,9 @@ void _mapPages(AmmState& state, std::uint64_t address, const std::vector") endif() + if(UNIX) + add_test_executable(guest_host_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardError.cpp) + target_include_directories(guest_host_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) + add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests) + set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10) + endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) @@ -278,6 +292,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) @@ -368,6 +391,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_attr PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_schedparam_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSchedparam.cpp) + target_include_directories(guest_pthread_schedparam_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_schedparam_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_schedparam_tests) + add_test(NAME guest_pthread_schedparam COMMAND guest_pthread_schedparam_tests) + set_tests_properties(guest_pthread_schedparam PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_schedparam PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_mutexattr_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexattr.cpp) target_include_directories(guest_pthread_mutexattr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_mutexattr_tests PRIVATE libkernel libc) @@ -395,7 +428,7 @@ if(BUILD_TESTING) add_test(NAME guest_pthread_start COMMAND guest_pthread_start_tests) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 RUN_SERIAL TRUE ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_attr_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrErrors.cpp) @@ -601,15 +634,6 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test_executable(guest_ioctl_getsid_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoctlGetsid.cpp) - target_include_directories(guest_ioctl_getsid_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_ioctl_getsid_tests PRIVATE libkernel libc) - configure_windows_unwind(guest_ioctl_getsid_tests) - add_test(NAME guest_ioctl_getsid COMMAND guest_ioctl_getsid_tests) - if(WIN32) - set_tests_properties(guest_ioctl_getsid PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) @@ -619,6 +643,15 @@ if(BUILD_TESTING) set_tests_properties(guest_sigtimedwait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_itimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestItimer.cpp) + target_include_directories(guest_itimer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_itimer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_itimer_tests) + add_test(NAME guest_itimer COMMAND guest_itimer_tests) + if(WIN32) + set_tests_properties(guest_itimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_parent_process_tests ${LIBS_INCLUDE_DIR}/tests/GuestParentProcess.cpp) target_include_directories(guest_parent_process_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_parent_process_tests PRIVATE libkernel libc) @@ -628,6 +661,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) @@ -765,6 +807,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_thermal_sensors_tests ${LIBS_INCLUDE_DIR}/tests/GuestThermalSensors.cpp) + target_include_directories(guest_thermal_sensors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_thermal_sensors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_thermal_sensors_tests) + add_test(NAME guest_thermal_sensors COMMAND guest_thermal_sensors_tests) + set_tests_properties(guest_thermal_sensors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_thermal_sensors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_getlogin_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetlogin.cpp) target_include_directories(guest_getlogin_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_getlogin_tests PRIVATE libkernel libc) @@ -852,6 +904,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getrlimit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_setrlimit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSetrlimit.cpp) + target_include_directories(guest_setrlimit_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_setrlimit_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_setrlimit_tests) + add_test(NAME guest_setrlimit COMMAND guest_setrlimit_tests) + set_tests_properties(guest_setrlimit PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_setrlimit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_equeue_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestEqueueEvents.cpp) target_include_directories(guest_equeue_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_equeue_event_tests PRIVATE libkernel libc) @@ -903,6 +965,15 @@ if(BUILD_TESTING) set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_jit_shared_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestJitSharedMemory.cpp) + target_include_directories(guest_jit_shared_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_jit_shared_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_jit_shared_memory_tests) + add_test(NAME guest_jit_shared_memory COMMAND guest_jit_shared_memory_tests) + if(WIN32) + set_tests_properties(guest_jit_shared_memory 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) @@ -921,6 +992,15 @@ if(BUILD_TESTING) set_tests_properties(guest_software_version PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_thread_sanitizer_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSanitizer.cpp) + target_include_directories(guest_thread_sanitizer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_thread_sanitizer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_thread_sanitizer_tests) + add_test(NAME guest_thread_sanitizer COMMAND guest_thread_sanitizer_tests) + if(WIN32) + set_tests_properties(guest_thread_sanitizer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_command_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemCommand.cpp) target_include_directories(guest_system_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_command_tests PRIVATE libkernel libc) @@ -961,6 +1041,7 @@ if(BUILD_TESTING) add_test(NAME guest_poll COMMAND guest_poll_tests) set_tests_properties(guest_poll PROPERTIES TIMEOUT 20) if(WIN32) + target_link_libraries(guest_poll_tests PRIVATE ws2_32) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -1002,6 +1083,26 @@ if(BUILD_TESTING) set_tests_properties(guest_dup PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kqueue_tests ${LIBS_INCLUDE_DIR}/tests/GuestKqueue.cpp) + target_include_directories(guest_kqueue_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kqueue_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kqueue_tests) + add_test(NAME guest_kqueue COMMAND guest_kqueue_tests) + set_tests_properties(guest_kqueue PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kqueue PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_socket_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketDescriptors.cpp) + target_include_directories(guest_socket_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_descriptors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_descriptors_tests) + add_test(NAME guest_socket_descriptors COMMAND guest_socket_descriptors_tests) + set_tests_properties(guest_socket_descriptors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp) target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc) @@ -1174,4 +1275,24 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_rename PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_coredump_attach_tests ${LIBS_INCLUDE_DIR}/tests/GuestCoredumpAttach.cpp) + target_include_directories(guest_coredump_attach_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_coredump_attach_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_coredump_attach_tests) + add_test(NAME guest_coredump_attach COMMAND guest_coredump_attach_tests) + set_tests_properties(guest_coredump_attach PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_coredump_attach PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_coredump_user_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestCoredumpUserData.cpp) + target_include_directories(guest_coredump_user_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_coredump_user_data_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_coredump_user_data_tests) + add_test(NAME guest_coredump_user_data COMMAND guest_coredump_user_data_tests) + set_tests_properties(guest_coredump_user_data PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_coredump_user_data PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 47befded43..a35b55b556 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -347,8 +347,6 @@ struct DirectMapping { }; std::map g_directMappings; - -#if defined(__linux__) struct PhysicalSpan { std::uint64_t end; std::vector bases; @@ -364,12 +362,10 @@ void SplitPhysicalSpan(std::uint64_t at) { g_physicalSpans.emplace_hint(std::next(span), at, PhysicalSpan{span->second.end, span->second.bases}); span->second.end = at; } -#endif std::map::iterator InsertMapping(std::uintptr_t base, DirectMapping mapping) { const auto [inserted, added] = g_directMappings.emplace(base, std::move(mapping)); if (!added) throw std::runtime_error("direct mapping inserted over a live one"); -#if defined(__linux__) const auto first = inserted->second.phys; const auto last = first + (inserted->second.end - base); SplitPhysicalSpan(first); @@ -381,12 +377,10 @@ std::map::iterator InsertMapping(std::uintptr_t b bases.insert(std::upper_bound(bases.begin(), bases.end(), base), base); cursor = span->second.end; } -#endif return inserted; } std::map::iterator RemoveMapping(std::map::iterator mapping) { -#if defined(__linux__) const auto base = mapping->first; const auto first = mapping->second.phys; const auto last = first + (mapping->second.end - base); @@ -406,10 +400,18 @@ std::map::iterator RemoveMapping(std::map MappingsSharingPhys(std::uint64_t first, std::uint64_t last) { + std::vector bases; + for (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.end() && span->first < last; ++span) bases.insert(bases.end(), span->second.bases.begin(), span->second.bases.end()); + if (auto span = g_physicalSpans.lower_bound(first); span != g_physicalSpans.begin() && (--span)->second.end > first) bases.insert(bases.end(), span->second.bases.begin(), span->second.bases.end()); + std::sort(bases.begin(), bases.end()); + bases.erase(std::unique(bases.begin(), bases.end()), bases.end()); + return bases; +} + void EraseMappings(std::uintptr_t start, std::uintptr_t end) { auto it = g_directMappings.lower_bound(start); if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; @@ -423,22 +425,20 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end) { } void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { - for (auto it = g_directMappings.begin(); it != g_directMappings.end();) { - const auto base = it->first; + for (const auto base : MappingsSharingPhys(first, last)) { + const auto it = g_directMappings.find(base); + if (it == g_directMappings.end()) continue; const auto mapping = it->second; const auto mappingLast = mapping.phys + (mapping.end - base); - if (mapping.phys >= last || mappingLast <= first) { - ++it; - continue; - } - it = RemoveMapping(it); + if (mapping.phys >= last || mappingLast <= first) continue; + RemoveMapping(it); if (mapping.phys < first) { const auto keep = first - mapping.phys; InsertMapping(base, DirectMapping{base + keep, mapping.phys, mapping.memoryType, mapping.backing}); } if (mappingLast > last) { const auto skip = last - mapping.phys; - it = InsertMapping(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}); + InsertMapping(base + skip, DirectMapping{mapping.end, last, mapping.memoryType, mapping.backing}); } } } @@ -458,15 +458,10 @@ void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { void WatchMapping(std::uintptr_t address, std::size_t len) { GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(reinterpret_cast(address), len); const auto end = address + len; - std::vector sharing; for (auto added = g_directMappings.lower_bound(address); added != g_directMappings.end() && added->first < end; ++added) { const auto addedPhys = added->second.phys; const auto addedPhysEnd = addedPhys + (added->second.end - added->first); - sharing.clear(); - for (auto span = g_physicalSpans.lower_bound(addedPhys); span != g_physicalSpans.end() && span->first < addedPhysEnd; ++span) sharing.insert(sharing.end(), span->second.bases.begin(), span->second.bases.end()); - std::sort(sharing.begin(), sharing.end()); - sharing.erase(std::unique(sharing.begin(), sharing.end()), sharing.end()); - for (const auto base : sharing) { + for (const auto base : MappingsSharingPhys(addedPhys, addedPhysEnd)) { const auto& other = g_directMappings.at(base); if ((base >= address && base < end) || other.backing != added->second.backing) continue; const auto first = std::max(addedPhys, other.phys); @@ -937,6 +932,29 @@ int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, i #endif } +int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { + ValidateOutput(addr); + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + const auto nativeProtection = LinuxProtFromSce(prot); + GuestAllocations::Mutation mutation; + void* mapped = MapPlaced(*addr, len, PROT_NONE, 0, PS5_PAGE_SIZE); + try { +#ifdef _WIN32 + GuestArena::GuestArenaMap_nid_postfix(mapped, len, object, 0, WinProtFromPosix(nativeProtection)); +#else + if (::mmap(mapped, len, nativeProtection, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); +#endif + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + Unmap(mapped, len); + throw; + } + *addr = mapped; + RecordProtection(mapped, len, prot); + Trace("map shared %p+0x%zx prot=0x%x", mapped, len, prot); + return 0; +} + #ifdef _WIN32 static bool IsGuestModuleImage(const void* base) { std::wstring path(MAX_PATH, L'\0'); @@ -966,6 +984,8 @@ int DoMprotect(const void* addr, size_t len, int prot) { const auto bytes = static_cast(end - first); const auto* pointer = reinterpret_cast(first); const auto nativeProtection = LinuxProtFromSce(prot); + if (GuestThreadStackHostRangeOverlaps(address, address + len)) + throw std::system_error(EACCES, std::generic_category(), "mprotect overlaps host-only guest thread stack pages"); std::uintptr_t stackStart = 0; std::uintptr_t stackEnd = 0; if ((prot & GuestProtGpuReadWrite) == 0 && GuestThreadStack(address, &stackStart, &stackEnd) && address >= stackStart && address + len <= stackEnd) { @@ -1132,7 +1152,8 @@ std::vector DirectMemoryViews(int64_t start, size_t len) { const auto last = first + len; std::vector views; std::lock_guard lock(g_directLock); - for (const auto& [base, mapping] : g_directMappings) { + for (const auto base : MappingsSharingPhys(first, last)) { + const auto& mapping = g_directMappings.at(base); const auto mappingLast = mapping.phys + (mapping.end - base); if (mapping.phys >= last || mappingLast <= first) continue; const auto viewStart = base + (std::max(first, mapping.phys) - mapping.phys); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index e6d1a5a603..c611faeabf 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -10,6 +10,12 @@ static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; +#ifdef _WIN32 +using HostSharedMemory = void*; +#else +using HostSharedMemory = int; +#endif + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); bool DirectMemoryCheckedFree(int64_t start, size_t len); @@ -31,6 +37,7 @@ size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE); int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); +int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object); int DoMprotect(const void* addr, size_t len, int prot); int DoMtypeprotect(const void* addr, size_t len, int type, int prot); int DoMunmap(void* addr, size_t len); @@ -38,5 +45,7 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment); bool GuestProtection(uintptr_t addr, int* prot); bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end); bool GuestRangeMapped(std::uintptr_t start, std::uintptr_t end); +void ForgetJitSharedMemory(int descriptor); +void DuplicateJitSharedMemory(int from, int to); #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 cf0fe12033..ea7e00b055 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -31,6 +31,8 @@ #include extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); +extern "C" void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; +extern "C" int* APS5_VABI __error_nid_postfix(); namespace { @@ -186,7 +188,8 @@ int APS5_VABI sceKernelMapDirectMemory2(void** addr, size_t len, int type, int p } int APS5_VABI sceKernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) { - return _mapFlexible(addr_in_out, len, prot, flags); + constexpr int IgnoredHeapFlag = 0x8000; + return _mapFlexible(addr_in_out, len, prot, flags & ~IgnoredHeapFlag); } int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const char* name); @@ -218,6 +221,13 @@ int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { return result; } +int APS5_VABI sceKernelMmap(void* addr, size_t len, int prot, int flags, int fd, int64_t offset, void** res) { + void* mapped = mmap_nid_postfix(addr, len, prot, flags, fd, offset); + if (mapped == reinterpret_cast(static_cast(-1))) return SceKernelError(*__error_nid_postfix()); + *res = mapped; + return 0; +} + int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { return sceKernelMunmap(reinterpret_cast(addr), len); } @@ -249,15 +259,7 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->end = reservedEnd; return 0; } - const auto lease = GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); - assert(std::is_sorted(lease.begin(), lease.end(), [](const auto& left, const auto& right) { return left->address < right->address; })); - const GuestAllocations::Range* best = nullptr; - for (const auto& range : lease) { - const auto begin = range->address; - const auto end = begin + range->bytes; - if (address >= begin && address < end) { best = range.get(); break; } - if ((flags & findNext) != 0 && begin > address && (best == nullptr || begin < best->address)) best = range.get(); - } + const auto best = GuestAllocations::GuestAllocationsLookup_nid_postfix(address, (flags & findNext) != 0); if (best != nullptr) { info->start = best->address; info->end = best->address + best->bytes; @@ -370,6 +372,12 @@ int APS5_VABI sceKernelConfiguredFlexibleMemorySize(size_t* size) { return 0; } +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(size_t* size) { + if (!size) return SCE_KERNEL_ERROR_EINVAL; + *size = 0; + return 0; +} + int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const char* name) { const auto start = reinterpret_cast(addr); if (!addr || len == 0 || !name || len > UINTPTR_MAX - start) return SCE_KERNEL_ERROR_EINVAL; @@ -388,11 +396,12 @@ 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) { - (void)cpu_total; - (void)cpu_available; - (void)gpu_total; - (void)gpu_available; - NotImplemented_nid_no_patch(__func__); + 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; return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp new file mode 100644 index 0000000000..ca31b70ce9 --- /dev/null +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -0,0 +1,216 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#else +#include +#include +#include +#endif + +namespace { + +constexpr int CpuRead = 0x1; +constexpr int CpuWrite = 0x2; +constexpr int CpuExecute = 0x4; +constexpr int CpuProtection = CpuRead | CpuWrite | CpuExecute; + +int Granted(int protection) { + return (protection & (CpuWrite | CpuExecute)) != 0 ? protection | CpuRead : protection; +} + +int KernelError(int error) { + switch (error) { + case EMFILE: + return SCE_KERNEL_ERROR_EMFILE; + case ENFILE: + return SCE_KERNEL_ERROR_ENFILE; + case ENOMEM: + case ENOSPC: + return SCE_KERNEL_ERROR_ENOMEM; + default: + return 0; + } +} + +int HostFailure(const char* operation, int error) { + if (const int kernel = KernelError(error)) return kernel; + throw std::system_error(error, std::generic_category(), operation); +} + +class JitObject { +public: +#ifdef _WIN32 + JitObject(std::size_t bytes, HANDLE section) : bytes(bytes), section(section) {} + ~JitObject() { CloseHandle(section); } +#else + explicit JitObject(std::size_t bytes) : bytes(bytes) {} +#endif + JitObject(const JitObject&) = delete; + JitObject& operator=(const JitObject&) = delete; + + std::size_t Bytes() const { return bytes; } + + HostSharedMemory Host(int descriptor) const { +#ifdef _WIN32 + static_cast(descriptor); + return section; +#else + return descriptor; +#endif + } + +private: + std::size_t bytes; +#ifdef _WIN32 + HANDLE section; +#endif +}; + +struct JitDescriptor { + std::shared_ptr object; + int maxProtection; +}; + +std::mutex g_jitLock; +std::map g_jitDescriptors; + +#ifdef _WIN32 +int WindowsFailure(const char* operation, DWORD error) { + if (error == ERROR_NOT_ENOUGH_MEMORY || error == ERROR_COMMITMENT_LIMIT || error == ERROR_DISK_FULL) return SCE_KERNEL_ERROR_ENOMEM; + throw std::system_error(static_cast(error), std::system_category(), operation); +} + +std::filesystem::path BackingPath() { + static std::atomic sequence{0}; + LARGE_INTEGER counter{}; + QueryPerformanceCounter(&counter); + const auto name = "anyps5-jit-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(sequence++) + "-" + std::to_string(counter.QuadPart); + return std::filesystem::temp_directory_path() / name; +} + +int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { + HANDLE file = CreateFileW(BackingPath().c_str(), GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr); + if (file == INVALID_HANDLE_VALUE) throw std::system_error(static_cast(GetLastError()), std::system_category(), "create JIT shared memory file"); + const auto size = static_cast(bytes); + HANDLE section = CreateFileMappingW(file, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + if (section == nullptr) { + const DWORD error = GetLastError(); + CloseHandle(file); + return WindowsFailure("create JIT shared memory section", error); + } + auto created = std::make_shared(bytes, section); + const int opened = _open_osfhandle(reinterpret_cast(file), _O_BINARY); + if (opened < 0) { + const int error = errno; + CloseHandle(file); + return HostFailure("open JIT shared memory descriptor", error); + } + *descriptor = opened; + *object = std::move(created); + return 0; +} + +int DuplicateDescriptor(int descriptor) { + return _dup(descriptor); +} +#else +int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { + const int file = memfd_create("anyps5 jit shared memory", MFD_CLOEXEC); + if (file < 0) return HostFailure("create JIT shared memory", errno); + const bool representable = bytes <= static_cast(std::numeric_limits::max()); + if (!representable || ftruncate(file, static_cast(bytes)) != 0) { + const int error = !representable || errno == EFBIG ? ENOMEM : errno; + ::close(file); + return HostFailure("size JIT shared memory", error); + } + *descriptor = file; + *object = std::make_shared(bytes); + return 0; +} + +int DuplicateDescriptor(int descriptor) { + return fcntl(descriptor, F_DUPFD_CLOEXEC, 0); +} +#endif + +} + +void ForgetJitSharedMemory(int descriptor) { + std::lock_guard lock(g_jitLock); + g_jitDescriptors.erase(descriptor); +} + +void DuplicateJitSharedMemory(int from, int to) { + std::lock_guard lock(g_jitLock); + g_jitDescriptors.erase(to); + const auto source = g_jitDescriptors.find(from); + if (source != g_jitDescriptors.end()) g_jitDescriptors.emplace(to, source->second); +} + +extern "C" { + +int APS5_VABI sceKernelJitCreateSharedMemory(const char* name, std::size_t len, int maxProt, int* fdOut) { + static_cast(name); + if (fdOut == nullptr || len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || (maxProt & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + int descriptor = -1; + std::shared_ptr object; + if (const int result = CreateBacking(len, &descriptor, &object)) return result; + std::lock_guard lock(g_jitLock); + g_jitDescriptors.insert_or_assign(descriptor, JitDescriptor{std::move(object), maxProt}); + *fdOut = descriptor; + return 0; +} + +int APS5_VABI sceKernelJitCreateAliasOfSharedMemory(int fd, int maxProt, int* fdOut) { + if (fdOut == nullptr || (maxProt & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(g_jitLock); + const auto source = g_jitDescriptors.find(fd); + if (source == g_jitDescriptors.end()) return SCE_KERNEL_ERROR_EBADF; + if ((Granted(maxProt) & ~Granted(source->second.maxProtection)) != 0) return SCE_KERNEL_ERROR_EACCES; + const int alias = DuplicateDescriptor(fd); + if (alias < 0) return HostFailure("duplicate JIT shared memory descriptor", errno); + g_jitDescriptors.insert_or_assign(alias, JitDescriptor{source->second.object, maxProt}); + *fdOut = alias; + return 0; +} + +int APS5_VABI sceKernelJitMapSharedMemory(int fd, int prot, void** startOut) { + if (startOut == nullptr || (prot & ~CpuProtection) != 0) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(g_jitLock); + const auto found = g_jitDescriptors.find(fd); + if (found == g_jitDescriptors.end()) return SCE_KERNEL_ERROR_EBADF; + if ((Granted(prot) & ~Granted(found->second.maxProtection)) != 0) return SCE_KERNEL_ERROR_EACCES; + const auto& object = *found->second.object; + void* address = *startOut; + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + if (arenaBytes != 0 && reinterpret_cast(address) - arenaBase >= arenaBytes) address = nullptr; + if (const int result = DoMapShared(&address, object.Bytes(), prot, object.Host(fd))) return result; + *startOut = address; + return 0; +} + +} 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/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 4792037637..1e314660cf 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -36,6 +36,8 @@ constexpr int GuestSyncAsync = 0x1; constexpr int GuestSyncInvalidate = 0x2; constexpr int GuestAdviceCore = 9; constexpr int GuestAdviceProtect = 10; +constexpr int GuestLockCurrent = 0x1; +constexpr int GuestLockFuture = 0x2; constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; constexpr int GuestAlignedSuper = 0x1000000; constexpr std::size_t SuperpageSize = std::size_t{1} << 21; @@ -207,6 +209,15 @@ int APS5_VABI munlock_nid_postfix(const void* address, std::size_t length) { return -1; } +int APS5_VABI mlockall_nid_postfix(int flags) { + if (flags == 0 || (flags & ~(GuestLockCurrent | GuestLockFuture)) != 0) { + SetError(GuestInvalid); + return -1; + } + if ((flags & GuestLockFuture) != 0) throw std::runtime_error("mlockall: MCL_FUTURE is not implemented: later mappings are not locked"); + throw std::runtime_error("mlockall: MCL_CURRENT is not implemented: the process also holds host stacks, heaps, modules and driver mappings"); +} + int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { const auto failed = [](int error) { SetError(error); @@ -225,7 +236,7 @@ int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { } int APS5_VABI madvise_nid_postfix(void* address, std::size_t length, int advice) { - if (advice == GuestAdviceProtect) NotImplemented_nid_no_patch("madvise MADV_PROTECT"); + if (advice == GuestAdviceProtect) return 0; const auto start = reinterpret_cast(address); if (advice < 0 || advice > GuestAdviceCore || start > GuestUserAddressEnd || length > GuestUserAddressEnd - start) { SetError(GuestInvalid); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index da6601e526..641deca1d4 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/File.hpp" #include #include @@ -16,7 +17,7 @@ static uint64_t g_nextEqueue = 1; extern "C" { -KernelEqueuePrivate::KernelEqueuePrivate(KernelEqueue handle) : m_handle(handle) {} +KernelEqueuePrivate::KernelEqueuePrivate(KernelEqueue handle, bool reportOncePerWait) : m_handle(handle), m_reportOncePerWait(reportOncePerWait) {} KernelEqueuePrivate::~KernelEqueuePrivate() { Close(); @@ -113,17 +114,10 @@ void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, } } -int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { - std::unique_lock lock(m_mutex); - if (m_closed) { - return SCE_KERNEL_ERROR_EBADF; - } - TriggerExpiredTimers(MonotonicNs()); - PollEvents(); - return CollectTriggered(ev, num); -} - int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { + if (m_reportOncePerWait) { + return CollectOncePerWait(ev, num); + } int ret = 0; for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { auto& e = *it; @@ -153,12 +147,60 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { return ret; } +int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { + int ret = 0; + std::list requeued; + for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { + auto& e = *it; + if (!e.triggered) { + ++it; + continue; + } + ev[ret++] = e.event; + if ((e.event.flags & EV_ONESHOT) != 0) { + if (e.filter.deleteEventFunc != nullptr) { + auto owner = e.filter.owner; + e.filter.deleteEventFunc(m_handle, &e); + } + it = m_events.erase(it); + continue; + } + if (e.filter.resetFunc != nullptr) { + e.filter.resetFunc(&e); + } else if ((e.event.flags & EV_CLEAR) != 0) { + e.triggered = false; + e.event.fflags = 0; + e.event.data = 0; + } + const auto next = std::next(it); + if (e.triggered) { + requeued.splice(requeued.end(), m_events, it); + } + it = next; + } + m_events.splice(m_events.end(), requeued); + return ret; +} + +int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { + std::unique_lock lock(m_mutex); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return CollectTriggered(ev, num); +} + int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { + return WaitForEventsUntil(ev, num, micros == 0 ? 0 : TimedWait::DeadlineNanos(micros)); +} + +int KernelEqueuePrivate::WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadline) { std::unique_lock lock(m_mutex); if (m_closed) { return SCE_KERNEL_ERROR_EBADF; } - const std::uint64_t deadline = TimedWait::DeadlineNanos(micros); for (;;) { TriggerExpiredTimers(MonotonicNs()); const bool polling = PollEvents(); @@ -169,7 +211,7 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros if (m_closed) { return SCE_KERNEL_ERROR_EBADF; } - if (micros == 0) { + if (deadline == 0) { uint32_t timerWait = 0; const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait); const std::uint64_t wake = hasTimer ? TimedWait::NowNanos() + static_cast(timerWait) * 1000ULL : 0; @@ -249,6 +291,32 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri return EQUEUE_OK; } +bool KernelEqueuePrivate::HasEvent(uintptr_t ident, int16_t filter) { + std::unique_lock lock(m_mutex); + return std::any_of(m_events.begin(), m_events.end(), [ident, filter](const auto& e) { + return e.event.ident == ident && e.event.filter == filter; + }); +} + +int KernelEqueuePrivate::EditEvent(uintptr_t ident, int16_t filter, const std::function& edit) { + std::unique_lock lock(m_mutex); + if (m_closed) { + return SCE_KERNEL_ERROR_EBADF; + } + auto it = std::find_if(m_events.begin(), m_events.end(), + [ident, filter](const auto& e) { + return e.event.ident == ident && e.event.filter == filter; + } + ); + if (it == m_events.end()) { + return SCE_KERNEL_ERROR_ENOENT; + } + edit(*it); + m_cond.NotifyOne(); + WakePollers(); + return EQUEUE_OK; +} + int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) { std::unique_lock lock(m_mutex); if (m_closed) { @@ -273,10 +341,20 @@ int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) { void KernelEqueuePrivate::RemoveDescriptorEvents(uintptr_t ident) { std::unique_lock lock(m_mutex); - const auto removed = std::erase_if(m_events, [ident](const auto& e) { - return e.event.ident == ident && (e.event.filter == EVFILT_READ || e.event.filter == EVFILT_WRITE); - }); - if (removed != 0) { + bool removed = false; + for (auto it = m_events.begin(); it != m_events.end();) { + if (it->event.ident != ident || (it->event.filter != EVFILT_READ && it->event.filter != EVFILT_WRITE && it->event.filter != EVFILT_VNODE)) { + ++it; + continue; + } + if (it->filter.deleteEventFunc != nullptr) { + auto owner = it->filter.owner; + it->filter.deleteEventFunc(m_handle, &*it); + } + it = m_events.erase(it); + removed = true; + } + if (removed) { WakePollers(); } } @@ -315,16 +393,6 @@ int APS5_VABI EqueueDeleteEvent_nid_postfix(KernelEqueue eq, uintptr_t ident, in } -int APS5_VABI kqueue_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI kevent_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name) { if (eq == nullptr || name == nullptr) { return SCE_KERNEL_ERROR_EINVAL; @@ -526,34 +594,17 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) { } static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) { - if (fd < GuestSockets::FirstDescriptor) { + const bool isSocket = GuestSockets::IsOpen(fd); + if (!isSocket && DescriptorIsOpen_nid_no_patch(fd)) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: non-socket descriptor" : "sceKernelAddWriteEvent: non-socket descriptor"); } if (size > 1) { NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark"); } - if (!GuestSockets::IsOpen(fd)) { + if (!isSocket) { return SCE_KERNEL_ERROR_EBADF; } - KernelEqueueEvent event{}; - event.event.ident = static_cast(fd); - event.event.filter = filter; - event.event.flags = EV_ADD; - event.event.udata = udata; - event.filter.pollFunc = [](KernelEqueueEvent* e) { - std::int64_t data = 0; - bool eof = false; - if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { - return false; - } - e->event.data = static_cast(data); - e->event.flags = static_cast(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF)); - return true; - }; - event.filter.resetFunc = [](KernelEqueueEvent* e) { - e->triggered = false; - }; - return EqueueAddEvent_nid_postfix(eq, event); + return EqueueAddEvent_nid_postfix(eq, EqueueDescriptorEvent(fd, filter, EV_ADD, udata)); } int APS5_VABI sceKernelAddReadEvent(KernelEqueue eq, int fd, std::size_t size, void* udata) { @@ -574,7 +625,30 @@ int APS5_VABI sceKernelDeleteWriteEvent(KernelEqueue eq, int fd) { } +KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata) { + KernelEqueueEvent event{}; + event.event.ident = static_cast(descriptor); + event.event.filter = filter; + event.event.flags = flags; + event.event.udata = udata; + event.filter.pollFunc = [](KernelEqueueEvent* e) { + std::int64_t data = 0; + bool eof = false; + if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { + return false; + } + e->event.data = static_cast(data); + e->event.flags = static_cast(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF)); + return true; + }; + event.filter.resetFunc = [](KernelEqueueEvent* e) { + e->triggered = false; + }; + return event; +} + void EqueueDescriptorClosed(int descriptor) { + KqueueDescriptorClosed(descriptor); std::vector owners; { std::unique_lock lock(g_equeueMutex); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index f5b291dbd0..8bc8d0671d 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,7 @@ static constexpr int16_t EVFILT_READ = -1; static constexpr int16_t EVFILT_WRITE = -2; +static constexpr int16_t EVFILT_VNODE = -4; static constexpr int16_t EVFILT_TIMER = -7; static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; @@ -24,8 +26,15 @@ static constexpr int16_t EVFILT_HRTIMER = -15; static constexpr int16_t EVFILT_AMPR = -25; static constexpr uint16_t EV_ADD = 0x0001; +static constexpr uint16_t EV_DELETE = 0x0002; +static constexpr uint16_t EV_ENABLE = 0x0004; +static constexpr uint16_t EV_DISABLE = 0x0008; static constexpr uint16_t EV_ONESHOT = 0x0010; static constexpr uint16_t EV_CLEAR = 0x0020; +static constexpr uint16_t EV_RECEIPT = 0x0040; +static constexpr uint16_t EV_DISPATCH = 0x0080; +static constexpr uint16_t EV_FORCEONESHOT = 0x0100; +static constexpr uint16_t EV_SYSFLAGS = 0xF000; static constexpr uint16_t EV_ERROR = 0x4000; static constexpr uint16_t EV_EOF = 0x8000; @@ -58,7 +67,7 @@ struct KernelEqueueEvent { class KernelEqueuePrivate { public: - explicit KernelEqueuePrivate(KernelEqueue handle); + explicit KernelEqueuePrivate(KernelEqueue handle, bool reportOncePerWait = false); ~KernelEqueuePrivate(); KernelEqueuePrivate(const KernelEqueuePrivate&) = delete; @@ -70,16 +79,20 @@ public: int AddEvent(const KernelEqueueEvent& event); int TriggerEvent(uintptr_t ident, int16_t filter, void* triggerData); int DeleteEvent(uintptr_t ident, int16_t filter); + bool HasEvent(uintptr_t ident, int16_t filter); + int EditEvent(uintptr_t ident, int16_t filter, const std::function& edit); int GetTriggeredEvents(KernelEvent* ev, int num); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); + int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); void RemoveDescriptorEvents(uintptr_t ident); void Close(); static uint64_t MonotonicNs(); private: void TriggerExpiredTimers(uint64_t nowNs); - int CollectTriggered(KernelEvent* ev, int num); bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const; + int CollectTriggered(KernelEvent* ev, int num); + int CollectOncePerWait(KernelEvent* ev, int num); bool PollEvents(); void WakePollers(); void WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos); @@ -90,11 +103,18 @@ private: TimedWait::Condition m_cond; std::string m_name; KernelEqueue m_handle; + bool m_reportOncePerWait; bool m_closed = false; }; using KernelEqueueRef = std::shared_ptr; void EqueueDescriptorClosed(int descriptor); +void KqueueDescriptorClosed(int descriptor); +bool IsKqueueDescriptor(int descriptor); +KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); +void RequireVnodeNotes(uint32_t fflags); +void SetVnodeNotes(KernelEqueueEvent& event, uint32_t fflags); +int AddVnodeEvent(const KernelEqueueRef& queue, int descriptor, uint32_t fflags, uint16_t flags, void* udata); extern "C" { KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq); diff --git a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp index 2cce0e556b..e72bfe3900 100644 --- a/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp +++ b/core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp @@ -4,6 +4,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" static const KernelEvent& requireEvent(const KernelEvent* ev, const char* caller) { if (ev == nullptr) { @@ -19,9 +20,8 @@ intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev) { } int APS5_VABI sceKernelGetEventError(const KernelEvent* ev) { - (void)ev; - NotImplemented_nid_no_patch(__func__); - return 0; + const KernelEvent& event = requireEvent(ev, __func__); + return (event.flags & EV_ERROR) != 0 ? static_cast(event.data) : 0; } intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev) { diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp new file mode 100644 index 0000000000..4540fa0c3b --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -0,0 +1,656 @@ +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#else +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace { + +constexpr int GUEST_EBADF = 9; +constexpr int GUEST_EINVAL = 22; +constexpr std::uint32_t NOTE_DELETE = 0x0001; +constexpr std::uint32_t NOTE_WRITE = 0x0002; +constexpr std::uint32_t NOTE_EXTEND = 0x0004; +constexpr std::uint32_t NOTE_ATTRIB = 0x0008; +constexpr std::uint32_t NOTE_LINK = 0x0010; +constexpr std::uint32_t NOTE_RENAME = 0x0020; +constexpr std::uint32_t MAPPED_NOTES = NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME; + +class VnodeWatch { +public: + VnodeWatch(const KernelEqueueRef& owner, std::uintptr_t descriptor, std::uint32_t notes) : queue(owner), ident(descriptor), interest(notes) { +#ifdef _WIN32 + stopEvent = ::CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (stopEvent == nullptr) { + throw std::runtime_error("EVFILT_VNODE: CreateEvent failed, error=" + std::to_string(::GetLastError())); + } +#else + stopEvent = ::eventfd(0, EFD_CLOEXEC); + if (stopEvent < 0) { + throw std::runtime_error("EVFILT_VNODE: eventfd failed, errno=" + std::to_string(errno)); + } +#endif + } + + ~VnodeWatch() { +#ifdef _WIN32 + ::CloseHandle(stopEvent); +#else + ::close(stopEvent); +#endif + } + + VnodeWatch(const VnodeWatch&) = delete; + VnodeWatch& operator=(const VnodeWatch&) = delete; + + void Stop() { + stopped.store(true); +#ifdef _WIN32 + ::SetEvent(stopEvent); +#else + const std::uint64_t one = 1; + const auto written = ::write(stopEvent, &one, sizeof(one)); + static_cast(written); +#endif + } + + void Deliver(std::uint32_t hint); + + std::uint32_t Interest() const { return interest.load(); } + void SetInterest(std::uint32_t notes) { interest.store(notes); } + +#ifdef _WIN32 + HANDLE StopEvent() const { return stopEvent; } +#else + int StopEvent() const { return stopEvent; } +#endif + +private: + std::weak_ptr queue; + std::uintptr_t ident; + std::atomic interest; + std::atomic stopped{false}; +#ifdef _WIN32 + HANDLE stopEvent = nullptr; +#else + int stopEvent = -1; +#endif +}; + +struct VnodeHint { + const VnodeWatch* watch; + std::uint32_t hint; +}; + +void VnodeWatch::Deliver(std::uint32_t hint) { + if (hint == 0 || stopped.load()) { + return; + } + const auto owner = queue.lock(); + if (!owner) { + return; + } + VnodeHint message{this, hint}; + owner->TriggerEvent(ident, EVFILT_VNODE, &message); +} + +#ifdef _WIN32 + +constexpr DWORD VNODE_POLL_MILLISECONDS = 50; +constexpr DWORD PARENT_CHANGES = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY; + +struct Snapshot { + std::uint64_t size = 0; + std::uint32_t links = 0; + std::uint32_t attributes = 0; + std::int64_t creation = 0; + std::int64_t lastWrite = 0; + std::int64_t change = 0; + bool deletePending = false; + std::wstring path; +}; + +bool TakeSnapshot(HANDLE handle, Snapshot& snapshot) { + FILE_BASIC_INFO basic{}; + FILE_STANDARD_INFO standard{}; + if (!::GetFileInformationByHandleEx(handle, FileBasicInfo, &basic, sizeof(basic)) || + !::GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard))) { + return false; + } + snapshot.size = static_cast(standard.EndOfFile.QuadPart); + snapshot.links = standard.NumberOfLinks; + snapshot.deletePending = standard.DeletePending != FALSE; + snapshot.attributes = basic.FileAttributes; + snapshot.creation = basic.CreationTime.QuadPart; + snapshot.lastWrite = basic.LastWriteTime.QuadPart; + snapshot.change = basic.ChangeTime.QuadPart; + std::wstring path(MAX_PATH, L'\0'); + auto length = ::GetFinalPathNameByHandleW(handle, path.data(), static_cast(path.size()), FILE_NAME_NORMALIZED); + if (length >= path.size()) { + path.resize(length); + length = ::GetFinalPathNameByHandleW(handle, path.data(), length, FILE_NAME_NORMALIZED); + } + if (length != 0 && length < path.size()) { + path.resize(length); + snapshot.path = std::move(path); + } + return true; +} + +std::vector SnapshotHints(const Snapshot& before, const Snapshot& after, bool directory) { + std::vector hints; + if (!directory) { + if (after.size > before.size) { + hints.push_back(NOTE_WRITE | NOTE_EXTEND); + } else if (after.size == before.size && after.lastWrite != before.lastWrite) { + hints.push_back(NOTE_WRITE); + } + if (after.links > before.links) { + hints.push_back(NOTE_LINK); + } + } + if (after.size < before.size || after.attributes != before.attributes || after.creation != before.creation) { + hints.push_back(NOTE_ATTRIB); + } else if (!directory && hints.empty() && after.change != before.change) { + hints.push_back(NOTE_ATTRIB); + } + if ((after.deletePending && !before.deletePending) || after.links < before.links) { + hints.push_back(NOTE_DELETE); + } + if (!after.path.empty() && after.path != before.path) { + hints.push_back(NOTE_RENAME); + } + return hints; +} + +HANDLE WatchParent(const std::wstring& path) { + const auto separator = path.find_last_of(L"\\/"); + if (separator == std::wstring::npos) { + return INVALID_HANDLE_VALUE; + } + const auto parent = path.substr(0, separator + 1); + const HANDLE handle = ::FindFirstChangeNotificationW(parent.c_str(), FALSE, PARENT_CHANGES); + const DWORD error = handle == INVALID_HANDLE_VALUE ? ::GetLastError() : ERROR_SUCCESS; + if (handle == INVALID_HANDLE_VALUE && error != ERROR_INVALID_FUNCTION) { + throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + } + return handle; +} + +HANDLE WatchContents(const std::wstring& path, DWORD changes) { + const HANDLE handle = ::FindFirstChangeNotificationW(path.c_str(), FALSE, changes); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD error = ::GetLastError(); + throw std::runtime_error("EVFILT_VNODE: FindFirstChangeNotification failed, error=" + std::to_string(error)); + } + return handle; +} + +struct HostWatch { + HANDLE target = INVALID_HANDLE_VALUE; + HANDLE parent = INVALID_HANDLE_VALUE; + HANDLE entries = INVALID_HANDLE_VALUE; + HANDLE subdirectories = INVALID_HANDLE_VALUE; + bool directory = false; + Snapshot last; +}; + +void CloseChange(HANDLE& handle) { + if (handle != INVALID_HANDLE_VALUE) { + ::FindCloseChangeNotification(handle); + handle = INVALID_HANDLE_VALUE; + } +} + +void CloseHostWatch(HostWatch& host) { + CloseChange(host.parent); + CloseChange(host.entries); + CloseChange(host.subdirectories); + if (host.target != INVALID_HANDLE_VALUE) { + ::CloseHandle(host.target); + host.target = INVALID_HANDLE_VALUE; + } +} + +bool Signaled(HANDLE handle) { + return handle != INVALID_HANDLE_VALUE && ::WaitForSingleObject(handle, 0) == WAIT_OBJECT_0; +} + +void RunHostWatch(std::shared_ptr watch, HostWatch host) { + for (;;) { + std::vector handles{watch->StopEvent()}; + for (const HANDLE handle : {host.parent, host.entries, host.subdirectories}) { + if (handle != INVALID_HANDLE_VALUE) { + handles.push_back(handle); + } + } + const DWORD result = ::WaitForMultipleObjects(static_cast(handles.size()), handles.data(), FALSE, VNODE_POLL_MILLISECONDS); + if (result == WAIT_OBJECT_0) { + break; + } + if (result == WAIT_FAILED) { + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: WaitForMultipleObjects failed, error=" + std::to_string(::GetLastError())); + } + std::vector hints; + if (Signaled(host.entries)) { + hints.push_back(NOTE_WRITE); + ::FindNextChangeNotification(host.entries); + } + if (Signaled(host.subdirectories)) { + hints.push_back(NOTE_WRITE | NOTE_LINK); + ::FindNextChangeNotification(host.subdirectories); + } + if (Signaled(host.parent)) { + ::FindNextChangeNotification(host.parent); + } + Snapshot current = host.last; + if (TakeSnapshot(host.target, current)) { + for (const auto hint : SnapshotHints(host.last, current, host.directory)) { + hints.push_back(hint); + } + if (current.path != host.last.path) { + CloseChange(host.parent); + host.parent = WatchParent(current.path); + } + host.last = std::move(current); + } + for (const auto hint : hints) { + watch->Deliver(hint); + } + } + CloseHostWatch(host); +} + +int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { + if (const auto directory = File::DirectoryDescriptorPath(descriptor)) { + host.directory = true; + host.target = ::CreateFileW(directory->wstring().c_str(), FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + return GUEST_EBADF; + } + const DWORD type = ::GetFileType(handle); + if (type == FILE_TYPE_PIPE) { + return GUEST_EINVAL; + } + if (type != FILE_TYPE_DISK) { + NotImplemented_nid_no_patch("EVFILT_VNODE: a descriptor that is not a file or a directory"); + } + host.target = ::ReOpenFile(handle, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0); + } + if (host.target == INVALID_HANDLE_VALUE) { + throw std::runtime_error("EVFILT_VNODE: opening the watched file failed, error=" + std::to_string(::GetLastError())); + } + if (!TakeSnapshot(host.target, host.last) || host.last.path.empty()) { + const auto error = ::GetLastError(); + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); + } + try { + host.parent = WatchParent(host.last.path); + if (host.directory) { + host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME); + host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME); + } + } catch (...) { + CloseHostWatch(host); + throw; + } + return 0; +} + +#else + +struct HostWatch { + int notify = -1; + int target = -1; + int entries = -1; + bool directory = false; + struct stat last {}; +}; + +struct HostEvent { + std::uint32_t mask; + std::uint32_t cookie; +}; + +void CloseHostWatch(HostWatch& host) { + for (int* descriptor : {&host.notify, &host.target, &host.entries}) { + if (*descriptor >= 0) { + ::close(*descriptor); + *descriptor = -1; + } + } +} + +std::uint32_t AttributeHint(HostWatch& host) { + struct stat current {}; + if (::fstat(host.target, ¤t) != 0) { + return NOTE_ATTRIB; + } + std::uint32_t hint = NOTE_ATTRIB; + if (current.st_nlink > host.last.st_nlink) { + hint = NOTE_LINK; + } else if (current.st_nlink < host.last.st_nlink) { + hint = host.directory && current.st_nlink != 0 ? NOTE_LINK : NOTE_DELETE; + } + host.last = current; + return hint; +} + +std::uint32_t ModifyHint(HostWatch& host) { + struct stat current {}; + if (::fstat(host.target, ¤t) != 0) { + return NOTE_WRITE; + } + std::uint32_t hint = NOTE_WRITE; + if (current.st_size > host.last.st_size) { + hint = NOTE_WRITE | NOTE_EXTEND; + } else if (current.st_size < host.last.st_size) { + hint = NOTE_ATTRIB; + } + host.last = current; + return hint; +} + +int ReadHostEvents(const HostWatch& host, std::vector& events) { + alignas(inotify_event) char buffer[16384]; + for (;;) { + const auto length = ::read(host.notify, buffer, sizeof(buffer)); + if (length < 0) { + if (errno == EINTR) { + continue; + } + return errno == EAGAIN ? 0 : errno; + } + if (length == 0) { + return 0; + } + for (char* cursor = buffer; cursor < buffer + length;) { + const auto* event = reinterpret_cast(cursor); + events.push_back({event->mask, event->cookie}); + cursor += sizeof(inotify_event) + event->len; + } + } +} + +std::unordered_map CountMoves(const std::vector& events) { + std::unordered_map moves; + for (const auto& event : events) { + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + ++moves[event.cookie]; + } + } + return moves; +} + +bool HasUnpairedMove(const std::vector& events, std::size_t first) { + const auto moves = CountMoves(events); + for (std::size_t index = first; index < events.size(); ++index) { + if ((events[index].mask & IN_MOVED_FROM) != 0 && moves.at(events[index].cookie) == 1) { + return true; + } + } + return false; +} + +int WaitForRenamesInProgress(const HostWatch& host) { + char entries[1024]; + if (::getdents64(host.entries, entries, sizeof(entries)) < 0 && errno != ENOENT) { + return errno; + } + return 0; +} + +int ReadSettledHostEvents(const HostWatch& host, std::vector& events) { + if (const int error = ReadHostEvents(host, events)) { + return error; + } + for (std::size_t settled = 0; HasUnpairedMove(events, settled);) { + settled = events.size(); + if (const int error = WaitForRenamesInProgress(host)) { + return error; + } + if (const int error = ReadHostEvents(host, events)) { + return error; + } + } + return 0; +} + +void RunHostWatch(std::shared_ptr watch, HostWatch host) { + pollfd waits[2] = {{host.notify, POLLIN, 0}, {watch->StopEvent(), POLLIN, 0}}; + bool watching = true; + while (watching) { + if (::poll(waits, 2, -1) < 0) { + if (errno == EINTR) { + continue; + } + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: poll failed, errno=" + std::to_string(errno)); + } + if (waits[1].revents != 0) { + break; + } + std::vector events; + if (const int error = ReadSettledHostEvents(host, events)) { + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: reading inotify failed, errno=" + std::to_string(error)); + } + const auto moves = CountMoves(events); + std::vector hints; + for (const auto& event : events) { + const std::uint32_t link = (event.mask & IN_ISDIR) != 0 ? NOTE_LINK : 0; + if ((event.mask & IN_MODIFY) != 0) { + hints.push_back(ModifyHint(host)); + } + if ((event.mask & IN_ATTRIB) != 0) { + hints.push_back(AttributeHint(host)); + } + if ((event.mask & (IN_CREATE | IN_DELETE)) != 0) { + hints.push_back(NOTE_WRITE | link); + } + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + hints.push_back(moves.at(event.cookie) > 1 ? NOTE_WRITE : NOTE_WRITE | NOTE_EXTEND | link); + } + if ((event.mask & IN_MOVE_SELF) != 0) { + hints.push_back(NOTE_RENAME); + } + if ((event.mask & IN_DELETE_SELF) != 0) { + hints.push_back(NOTE_DELETE); + } + if ((event.mask & IN_IGNORED) != 0) { + watching = false; + } + } + for (const auto hint : hints) { + watch->Deliver(hint); + } + } + CloseHostWatch(host); +} + +int OpenHostWatch(int descriptor, std::uint32_t, HostWatch& host) { + struct stat status {}; + if (::fstat(descriptor, &status) != 0) { + return GUEST_EBADF; + } + if (S_ISFIFO(status.st_mode) || S_ISSOCK(status.st_mode)) { + return GUEST_EINVAL; + } + if (!S_ISREG(status.st_mode) && !S_ISDIR(status.st_mode)) { + NotImplemented_nid_no_patch("EVFILT_VNODE: a descriptor that is not a file or a directory"); + } + host.directory = S_ISDIR(status.st_mode); + const auto path = "/proc/self/fd/" + std::to_string(descriptor); + host.target = ::open(path.c_str(), O_PATH | O_CLOEXEC); + if (host.target < 0 || ::fstat(host.target, &host.last) != 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: opening the watched file failed, errno=" + std::to_string(error)); + } + if (host.directory) { + host.entries = ::open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (host.entries < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: opening the watched directory failed, errno=" + std::to_string(error)); + } + } + host.notify = ::inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + std::uint32_t mask = IN_MODIFY | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; + if (host.directory) { + mask |= IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO; + } + if (host.notify < 0 || ::inotify_add_watch(host.notify, path.c_str(), mask) < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: inotify failed, errno=" + std::to_string(error)); + } + return 0; +} + +#endif + +void TriggerVnode(KernelEqueueEvent* event, void* data) { + const auto* message = static_cast(data); + if (event->filter.data != message->watch) { + return; + } + if ((message->watch->Interest() & message->hint) != 0) { + event->event.fflags |= message->hint; + } + event->triggered = event->event.fflags != 0; +} + +void StopVnode(KernelEqueue, KernelEqueueEvent* event) { + static_cast(event->filter.data)->Stop(); +} + +int VnodeDescriptorError(int descriptor) { + if (descriptor < 0) { + return GUEST_EBADF; + } + if (GuestSockets::IsOpen(descriptor)) { + return GUEST_EINVAL; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + return GUEST_EBADF; + } + return IsKqueueDescriptor(descriptor) ? GUEST_EINVAL : 0; +} + +} + +void RequireVnodeNotes(std::uint32_t fflags) { + if ((fflags & ~MAPPED_NOTES) != 0) { + NotImplemented_nid_no_patch(("EVFILT_VNODE: fflags " + std::to_string(fflags & ~MAPPED_NOTES)).c_str()); + } +} + +void SetVnodeNotes(KernelEqueueEvent& event, std::uint32_t fflags) { + static_cast(event.filter.data)->SetInterest(fflags); +} + +int AddVnodeEvent(const KernelEqueueRef& queue, int descriptor, std::uint32_t fflags, std::uint16_t flags, void* udata) { + RequireVnodeNotes(fflags); + if (const int error = VnodeDescriptorError(descriptor)) { + return error; + } + HostWatch host; + if (const int error = OpenHostWatch(descriptor, fflags, host)) { + return error; + } + std::shared_ptr watch; + try { + watch = std::make_shared(queue, static_cast(descriptor), fflags); + } catch (...) { + CloseHostWatch(host); + throw; + } + KernelEqueueEvent event{}; + event.event.ident = static_cast(descriptor); + event.event.filter = EVFILT_VNODE; + event.event.flags = flags; + event.event.udata = udata; + event.filter.data = watch.get(); + event.filter.owner = watch; + event.filter.triggerFunc = TriggerVnode; + event.filter.deleteEventFunc = StopVnode; + const int result = queue->AddEvent(event); + bool registered = false; + if (result == EQUEUE_OK) { + queue->EditEvent(event.event.ident, EVFILT_VNODE, [®istered, &watch](KernelEqueueEvent& added) { + registered = added.filter.data == watch.get(); + }); + } + if (!registered) { + CloseHostWatch(host); + return result == EQUEUE_OK ? 0 : result & 0xffff; + } + std::thread(RunHostWatch, watch, std::move(host)).detach(); + return 0; +} + +extern "C" { + +int APS5_VABI sceKernelAddFileEvent(KernelEqueue eq, int fd, int watch, void* udata) { + const auto queue = EqueuePin_nid_postfix(eq); + if (!queue) { + return SCE_KERNEL_ERROR_EBADF; + } + const auto notes = static_cast(watch); + RequireVnodeNotes(notes); + if (const int error = VnodeDescriptorError(fd)) { + return SceKernelError(error); + } + const int edited = queue->EditEvent(static_cast(fd), EVFILT_VNODE, [notes, udata](KernelEqueueEvent& event) { + event.event.udata = udata; + SetVnodeNotes(event, notes); + }); + if (edited != SCE_KERNEL_ERROR_ENOENT) { + return edited; + } + const int error = AddVnodeEvent(queue, fd, notes, EV_CLEAR, udata); + return error == 0 ? EQUEUE_OK : SceKernelError(error); +} + +int APS5_VABI sceKernelDeleteFileEvent(KernelEqueue eq, int fd) { + if (!EqueuePin_nid_postfix(eq)) { + return SCE_KERNEL_ERROR_EBADF; + } + if (fd < 0 || (!GuestSockets::IsOpen(fd) && !DescriptorIsOpen_nid_no_patch(fd))) { + return SCE_KERNEL_ERROR_EBADF; + } + return EqueueDeleteEvent_nid_postfix(eq, static_cast(fd), EVFILT_VNODE); +} + +} diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp new file mode 100644 index 0000000000..1696650661 --- /dev/null +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -0,0 +1,398 @@ +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); + +namespace { + +constexpr int GUEST_ENOENT = 2; +constexpr int GUEST_EBADF = 9; +constexpr int GUEST_EFAULT = 14; +constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_ENFILE = 23; +constexpr int GUEST_EMFILE = 24; + +constexpr int KQ_NEVENTS = 8; +constexpr std::uint32_t NOTE_FFCTRLMASK = 0xc0000000; +constexpr std::uint32_t NOTE_FFLAGSMASK = 0x00ffffff; +constexpr std::uint32_t NOTE_FFAND = 0x40000000; +constexpr std::uint32_t NOTE_FFOR = 0x80000000; +constexpr std::uint32_t NOTE_FFCOPY = 0xc0000000; +constexpr std::uint32_t NOTE_TRIGGER = 0x01000000; +constexpr std::uint32_t NOTE_LOWAT = 0x0001; +constexpr std::uint32_t NOTE_SECONDS = 0x0001; +constexpr std::uint32_t NOTE_MSECONDS = 0x0002; +constexpr std::uint32_t NOTE_USECONDS = 0x0004; +constexpr std::uint32_t NOTE_NSECONDS = 0x0008; +constexpr std::uint32_t NOTE_TIMER_PRECMASK = NOTE_SECONDS | NOTE_MSECONDS | NOTE_USECONDS | NOTE_NSECONDS; +constexpr std::uint16_t KNOTE_CLEARED_FLAGS = EV_ADD | EV_DELETE | EV_ENABLE | EV_DISABLE | EV_FORCEONESHOT; + +std::mutex g_kqueueMutex; +std::unordered_map g_kqueues; + +int PosixFailure(int error) { + *__error_nid_postfix() = error; + return -1; +} + +int GuestErrno(int sceError) { + return sceError & 0xffff; +} + +KernelEqueueRef FindKqueue(int descriptor) { + std::lock_guard lock(g_kqueueMutex); + const auto found = g_kqueues.find(descriptor); + return found != g_kqueues.end() ? found->second : KernelEqueueRef{}; +} + +int ReserveDescriptor() { +#ifdef _WIN32 + return ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + return ::open("/dev/null", O_RDONLY | O_CLOEXEC); +#endif +} + +void ReleaseDescriptor(int descriptor) { +#ifdef _WIN32 + ::_close(descriptor); +#else + ::close(descriptor); +#endif +} + +std::optional TimerPeriodNanos(std::int64_t data, std::uint32_t fflags) { + std::uint64_t unit = 0; + switch (fflags & NOTE_TIMER_PRECMASK) { + case NOTE_SECONDS: + unit = 1000000000ULL; + break; + case NOTE_MSECONDS: + case 0: + unit = 1000000ULL; + break; + case NOTE_USECONDS: + unit = 1000ULL; + break; + case NOTE_NSECONDS: + if (data >= 1000000000LL && data % 1000000000LL != 0) { + NotImplemented_nid_no_patch("kevent: EVFILT_TIMER NOTE_NSECONDS period above one second with a fraction"); + } + unit = 1; + break; + default: + return std::nullopt; + } + const auto value = static_cast(data); + if (value > std::numeric_limits::max() / unit) { + return std::numeric_limits::max(); + } + return value * unit; +} + +int DescriptorFilterError(int kqueueDescriptor, std::uintptr_t ident) { + if (ident > static_cast(INT_MAX)) { + return GUEST_EBADF; + } + const int descriptor = static_cast(ident); + if (GuestSockets::IsOpen(descriptor)) { + return 0; + } + if (!DescriptorIsOpen_nid_no_patch(descriptor)) { + return GUEST_EBADF; + } + return descriptor == kqueueDescriptor ? GUEST_EINVAL : 0; +} + +int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + const int descriptor = static_cast(change.ident); + if (IsKqueueDescriptor(descriptor)) { + if (change.filter != EVFILT_READ) { + return GUEST_EINVAL; + } + NotImplemented_nid_no_patch("kevent: EVFILT_READ on a kqueue descriptor"); + } + if (!GuestSockets::IsOpen(descriptor)) { + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a non-socket descriptor" : "kevent: EVFILT_WRITE on a non-socket descriptor"); + } + if ((change.fflags & NOTE_LOWAT) != 0) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT"); + } + if ((flags & EV_CLEAR) != 0) { + NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket"); + } + auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +int AddTimerKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + std::int64_t period = change.data; + if (period < 0) { + return GUEST_EINVAL; + } + if (period == 0 && (flags & EV_ONESHOT) == 0) { + period = 1; + } + if ((change.fflags & ~NOTE_TIMER_PRECMASK) != 0) { + return GUEST_EINVAL; + } + const auto periodNs = TimerPeriodNanos(period, change.fflags); + if (!periodNs) { + return GUEST_EINVAL; + } + const std::uint64_t now = KernelEqueuePrivate::MonotonicNs(); + KernelEqueueEvent event{}; + event.deadlineNs = *periodNs <= std::numeric_limits::max() - now ? now + *periodNs : std::numeric_limits::max(); + event.intervalNs = (flags & EV_ONESHOT) != 0 ? 0 : *periodNs; + event.event = change; + event.event.flags = static_cast(flags | EV_CLEAR); + event.event.fflags = 0; + event.event.data = 0; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +int AddUserKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { + KernelEqueueEvent event{}; + event.event = change; + event.event.flags = flags; + event.filter.resetFunc = [](KernelEqueueEvent* e) { + if ((e->event.flags & EV_CLEAR) != 0) { + e->triggered = false; + } + }; + change.fflags = 0; + change.data = 0; + return GuestErrno(queue->AddEvent(event)); +} + +void TouchUserKnote(KernelEqueueEvent& event, KernelEvent& change) { + if ((change.fflags & NOTE_TRIGGER) != 0) { + event.triggered = true; + } + const std::uint32_t control = change.fflags & NOTE_FFCTRLMASK; + change.fflags &= NOTE_FFLAGSMASK; + if (control == NOTE_FFAND) { + event.event.fflags &= change.fflags; + } else if (control == NOTE_FFOR) { + event.event.fflags |= change.fflags; + } else if (control == NOTE_FFCOPY) { + event.event.fflags = change.fflags; + } + event.event.data = change.data; + if ((change.flags & EV_CLEAR) != 0) { + event.triggered = false; + } +} + +int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { + if (change.filter == EVFILT_TIMER) { + NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); + } + if ((change.filter == EVFILT_READ || change.filter == EVFILT_WRITE) && (change.fflags & NOTE_LOWAT) != 0) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT"); + } + if (change.filter == EVFILT_VNODE) { + RequireVnodeNotes(change.fflags); + } + return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change](KernelEqueueEvent& event) { + event.event.udata = change.udata; + if (change.filter == EVFILT_USER) { + TouchUserKnote(event, change); + } else if (change.filter == EVFILT_VNODE) { + SetVnodeNotes(event, change.fflags); + } + })); +} + +int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& change) { + if ((change.flags & (EV_ENABLE | EV_DISABLE)) == (EV_ENABLE | EV_DISABLE)) { + return GUEST_EINVAL; + } + if (change.filter > 0) { + return GUEST_EINVAL; + } + if (change.filter != EVFILT_READ && change.filter != EVFILT_WRITE && change.filter != EVFILT_VNODE && change.filter != EVFILT_TIMER && change.filter != EVFILT_USER) { + NotImplemented_nid_no_patch(("kevent: filter " + std::to_string(change.filter)).c_str()); + } + if ((change.flags & EV_DISABLE) != 0) { + NotImplemented_nid_no_patch("kevent: EV_DISABLE"); + } + if ((change.flags & EV_DISPATCH) != 0) { + NotImplemented_nid_no_patch("kevent: EV_DISPATCH"); + } + if ((change.flags & EV_FORCEONESHOT) != 0) { + NotImplemented_nid_no_patch("kevent: EV_FORCEONESHOT"); + } + if (change.filter == EVFILT_READ || change.filter == EVFILT_WRITE || change.filter == EVFILT_VNODE) { + if (const int error = DescriptorFilterError(kqueueDescriptor, change.ident)) { + return error; + } + } + if (!queue->HasEvent(change.ident, change.filter)) { + if ((change.flags & EV_ADD) == 0) { + return GUEST_ENOENT; + } + const auto flags = static_cast(change.flags & ~KNOTE_CLEARED_FLAGS); + if (change.filter == EVFILT_TIMER) { + return AddTimerKnote(queue, change, flags); + } + if (change.filter == EVFILT_USER) { + return AddUserKnote(queue, change, flags); + } + if (change.filter == EVFILT_VNODE) { + const std::uint32_t notes = change.fflags; + change.fflags = 0; + change.data = 0; + return AddVnodeEvent(queue, static_cast(change.ident), notes, flags, change.udata); + } + return AddDescriptorKnote(queue, change, flags); + } + if ((change.flags & EV_DELETE) != 0) { + return GuestErrno(queue->DeleteEvent(change.ident, change.filter)); + } + return ModifyKnote(queue, change); +} + +std::optional ScanDeadline(const KernelTimespec& timeout) { + if (timeout.tv_sec > INT32_MAX) { + return std::nullopt; + } + const std::uint64_t span = static_cast(timeout.tv_sec) * 1000000000ULL + static_cast(timeout.tv_nsec); + return TimedWait::NowNanos() + span; +} + +} + +bool IsKqueueDescriptor(int descriptor) { + return static_cast(FindKqueue(descriptor)); +} + +void KqueueDescriptorClosed(int descriptor) { + KernelEqueueRef closed; + std::vector queues; + { + std::lock_guard lock(g_kqueueMutex); + const auto found = g_kqueues.find(descriptor); + if (found != g_kqueues.end()) { + closed = std::move(found->second); + g_kqueues.erase(found); + } + for (const auto& [kqueueDescriptor, queue] : g_kqueues) { + queues.push_back(queue); + } + } + if (closed) { + closed->Close(); + } + for (const auto& queue : queues) { + queue->RemoveDescriptorEvents(static_cast(descriptor)); + } +} + +extern "C" { + +int APS5_VABI kqueue_nid_postfix() { + const int descriptor = ReserveDescriptor(); + if (descriptor < 0) { + if (errno == EMFILE) return PosixFailure(GUEST_EMFILE); + if (errno == ENFILE) return PosixFailure(GUEST_ENFILE); + throw std::runtime_error(std::string(__func__) + ": reserving a descriptor failed, errno=" + std::to_string(errno)); + } + auto queue = std::make_shared(static_cast(descriptor), true); + queue->SetName("kqueue"); + std::lock_guard lock(g_kqueueMutex); + g_kqueues[descriptor] = std::move(queue); + return descriptor; +} + +int APS5_VABI kevent_nid_postfix(int descriptor, const KernelEvent* changelist, int nchanges, KernelEvent* eventlist, int nevents, const KernelTimespec* timeout) { + const auto queue = FindKqueue(descriptor); + if (!queue) { + return PosixFailure(GUEST_EBADF); + } + if (nevents < 0) { + NotImplemented_nid_no_patch("kevent: negative nevents"); + } + if (nchanges > 0 && changelist == nullptr) { + return PosixFailure(GUEST_EFAULT); + } + int errors = 0; + for (int i = 0; i < nchanges; ++i) { + KernelEvent change = changelist[i]; + if (change.filter == 0) { + continue; + } + change.flags = static_cast(change.flags & ~EV_SYSFLAGS); + const int error = Register(queue, descriptor, change); + if (error == 0 && (change.flags & EV_RECEIPT) == 0) { + continue; + } + if (nevents == 0) { + return error == 0 ? 0 : PosixFailure(error); + } + change.flags = EV_ERROR; + change.data = error; + if (eventlist != nullptr) { + *eventlist++ = change; + } + --nevents; + ++errors; + } + if (errors != 0) { + return errors; + } + if (nevents == 0) { + return 0; + } + std::optional deadline; + bool poll = false; + if (timeout != nullptr) { + if (timeout->tv_sec < 0 || timeout->tv_nsec < 0 || timeout->tv_nsec >= 1000000000LL) { + return PosixFailure(GUEST_EINVAL); + } + poll = timeout->tv_sec == 0 && timeout->tv_nsec == 0; + if (!poll) { + deadline = ScanDeadline(*timeout); + } + } + KernelEvent scratch[KQ_NEVENTS]; + KernelEvent* target = eventlist != nullptr ? eventlist : scratch; + const int capacity = eventlist != nullptr ? nevents : std::min(nevents, KQ_NEVENTS); + const int count = poll ? queue->GetTriggeredEvents(target, capacity) : queue->WaitForEventsUntil(target, capacity, deadline.value_or(0)); + if (count < 0) { + return PosixFailure(GuestErrno(count)); + } + if (count > 0 && eventlist == nullptr) { + return PosixFailure(GUEST_EFAULT); + } + return count; +} + +} diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index a8ce99649a..a740c9ac0a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -13,6 +13,7 @@ namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); +void DuplicateDirectoryDescriptor(int from, int to); std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); 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/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 5715f02119..0d0e003697 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -6,6 +6,7 @@ namespace File { +std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath); void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb); void FillFileStat(int nativeDescriptor, FileStat* sb); bool FillFileStatFromDescriptor(int fd, FileStat* sb); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index d2a62b7394..ae0849bb98 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -84,7 +85,9 @@ constexpr int SettableStatusFlags = OpenStatusFlags | ReadAheadFlag; constexpr std::int16_t ReadLock = 1; constexpr std::int16_t Unlock = 2; constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromStart = 0; constexpr std::int16_t FromCurrent = 1; +constexpr std::int16_t FromEnd = 2; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; @@ -212,6 +215,62 @@ int Readable(int fd, Kind kind, const char* function) { } return static_cast(available); } + +std::int64_t LockOffset(int fd, Kind kind) { + if (kind == Kind::Directory) { + const auto cursor = File::SeekDirectoryDescriptor(fd, 0, FromCurrent); + if (!cursor) throw std::runtime_error("fcntl: cannot read the offset of directory descriptor " + std::to_string(fd)); + return *cursor; + } + const auto offset = ::_telli64(fd); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return offset; +} + +std::int64_t LockFileSize(int fd, Kind kind) { + if (kind == Kind::Directory) Unsupported("fcntl", "a record lock from the end", kind, fd); + const auto size = ::_filelengthi64(fd); + if (size < 0) throw std::runtime_error("fcntl: cannot read the size of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return size; +} + +int LockRangeError(int fd, Kind kind, const GuestLock& lock) { + constexpr auto Largest = std::numeric_limits::max(); + std::int64_t start = lock.start; + if (lock.whence == FromEnd) { + const auto size = LockFileSize(fd, kind); + if (lock.start > 0 && size > Largest - lock.start) return GuestEoverflow; + start = size + lock.start; + } else if (lock.whence != FromStart && lock.whence != FromCurrent) { + return GuestEinval; + } + if (start < 0) return GuestEinval; + if (lock.length < 0 && (start == 0 || start + lock.length < 0)) return GuestEinval; + if (lock.length > 0 && lock.length - 1 > Largest - start) return GuestEoverflow; + return 0; +} + +int ProcessRecordLock(int fd, Kind kind, int command, GuestLock& lock, void* pointer, std::size_t size) { + const bool known = lock.type == ReadLock || lock.type == WriteLock || lock.type == Unlock; + if (command == GetLock && !known) return Fail(GuestEinval); + if (lock.whence == FromCurrent) { + const auto offset = LockOffset(fd, kind); + if (lock.start > 0 && offset > std::numeric_limits::max() - lock.start) return Fail(GuestEoverflow); + lock.start += offset; + } + if (command != GetLock) { + if (!known) return Fail(GuestEinval); + const int access = lock.type == Unlock ? ReadWrite : AccessMode(fd, "fcntl"); + if ((lock.type == ReadLock && access == WriteOnly) || (lock.type == WriteLock && access == ReadOnly)) return Fail(GuestEbadf); + } + if (const int error = LockRangeError(fd, kind, lock); error != 0) return Fail(error); + if (command != GetLock) return 0; + lock.type = Unlock; + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +} #else std::optional Classify(int fd, const char* function) { if (fd < 0) return std::nullopt; @@ -247,7 +306,9 @@ Entry InitialEntry(int fd) { if (host & O_APPEND) status |= Append; if (host & O_ASYNC) status |= Asynchronous; if ((host & O_SYNC) == O_SYNC) status |= Synchronous; +#ifdef O_DIRECT if (host & O_DIRECT) status |= Direct; +#endif return {(descriptorFlags & FD_CLOEXEC) != 0, std::make_shared(status)}; } @@ -329,8 +390,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); #ifdef _WIN32 - static_cast(current); - throw std::runtime_error("fcntl: record locks are not implemented on Windows, command=" + std::to_string(command) + ", fd=" + std::to_string(fd)); + return ProcessRecordLock(fd, *kind, current, lock, pointer, size); #else struct flock host{}; if (lock.type == ReadLock) host.l_type = F_RDLCK; @@ -505,6 +565,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/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index de24d4eacc..05868357b8 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +88,15 @@ void ForgetDirectoryDescriptor(int fd) { g_directoryCount = g_directories.size(); } +void DuplicateDirectoryDescriptor(int from, int to) { + if (g_directoryCount == 0) return; + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(from); + if (found == g_directories.end()) return; + g_directories[to] = found->second; + g_directoryCount = g_directories.size(); +} + std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { constexpr int SeekSet = 0; constexpr int SeekCurrent = 1; @@ -128,8 +138,7 @@ int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; if (name.size() > 255) { - if (used == 0) return SCE_KERNEL_ERROR_ENAMETOOLONG; - break; + throw std::runtime_error("Directory entry name exceeds the guest 255-byte limit"); } const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; if (used + record > static_cast(nbytes)) { diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 4495fb59f9..5e6e304a73 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -125,6 +125,12 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { namespace File { +std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath) { + std::error_code error; + if (std::filesystem::exists(std::filesystem::symlink_status(nativePath, error))) return nativePath; + return RelinkedModulePath_nid_no_patch(nativePath); +} + void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { NativeStat st{}; if (DoStat(nativePath, &st) != 0) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index f9a6944ca0..8811f3e4b1 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" @@ -11,6 +12,7 @@ #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" #include @@ -42,13 +44,19 @@ 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); - return result == -2 ? ::_lseeki64(fd, offset, whence) : result; + if (result != -2) return result; + const auto position = ::_lseeki64(fd, offset, whence); + if (position < 0 || GetFileType(reinterpret_cast(::_get_osfhandle(fd))) != FILE_TYPE_PIPE) return position; + errno = ESPIPE; + return -1; } 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; return result; @@ -57,6 +65,7 @@ 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)); @@ -208,6 +217,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); + if ((flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY) native = File::PathOrRelinkedModule(native); return OpenNative(native, flags, mode); } @@ -219,9 +229,11 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { - if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); + if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); + EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); + ForgetJitSharedMemory(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -234,7 +246,7 @@ int APS5_VABI sceKernelClose(int d) { } std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { - if (d >= GuestSockets::FirstDescriptor) { + if (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Read(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } @@ -250,7 +262,7 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { } std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes) { - if (d >= GuestSockets::FirstDescriptor) { + if (GuestSockets::IsOpen(d)) { const auto n = GuestSockets::Write(d, buf, nbytes); return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } @@ -263,6 +275,7 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes } std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { + if (GuestSockets::IsOpen(d)) return SCE_KERNEL_ERROR_ESPIPE; if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } @@ -287,6 +300,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { 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; const auto status = std::filesystem::status(native, error); @@ -336,9 +350,12 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { #endif } int result; - if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, argument); - else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); - else result = fcntl_nid_postfix(d, command); + if (GuestSockets::IsOpen(d)) { + if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else result = fcntl_nid_postfix(d, command); + } else { + result = File::Fcntl(d, command, argument); + } return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 1c5a227a9d..f2027dd101 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -6,6 +6,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" @@ -16,6 +17,7 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include @@ -24,9 +26,13 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include struct KernelIovec { @@ -123,9 +129,9 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { } return SetFileTime(handle, nullptr, &access, &modified) ? 0 : File::WindowsFileTime::Failure(GetLastError()); } -static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times) { +static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { const auto handle = CreateFileW(path.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | (followLink ? 0 : FILE_FLAG_OPEN_REPARSE_POINT), nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); const int result = SetTimes(handle, times); CloseHandle(handle); @@ -167,6 +173,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"); @@ -180,6 +207,7 @@ 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; @@ -206,6 +234,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; @@ -224,6 +253,7 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb } #else #include +#include #include #include #include @@ -253,11 +283,11 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { return ::truncate(path.c_str(), static_cast(length)) == 0 ? 0 : errno; } -static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times) { - if (times == nullptr) return ::utimes(path.c_str(), nullptr); +static int NativeUtimes(const std::filesystem::path& path, const KernelTimeval* times, bool followLink = true) { + if (times == nullptr) return followLink ? ::utimes(path.c_str(), nullptr) : ::lutimes(path.c_str(), nullptr); struct timeval values[2]{{static_cast(times[0].tv_sec), static_cast(times[0].tv_usec)}, {static_cast(times[1].tv_sec), static_cast(times[1].tv_usec)}}; - return ::utimes(path.c_str(), values); + return followLink ? ::utimes(path.c_str(), values) : ::lutimes(path.c_str(), values); } static int NativeFutimes(int descriptor, const KernelTimeval* times) { if (times == nullptr) return ::futimes(descriptor, nullptr); @@ -283,6 +313,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; @@ -294,6 +325,7 @@ 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; @@ -303,13 +335,12 @@ static constexpr int GUEST_EOVERFLOW = 84; static int SceErrorFromErrno(int error); static std::int64_t PositionedGuestSocketError(int descriptor) { - if (descriptor < GuestSockets::FirstDescriptor) return 0; - return SceErrorFromErrno(GuestSockets::IsOpen(descriptor) ? GUEST_ESPIPE : GUEST_EBADF); + return GuestSockets::IsOpen(descriptor) ? SceErrorFromErrno(GUEST_ESPIPE) : 0; } static bool FailPosixPositionedGuestSocket(int descriptor) { - if (descriptor < GuestSockets::FirstDescriptor) return false; - errno = GuestSockets::IsOpen(descriptor) ? GUEST_ESPIPE : GUEST_EBADF; + if (!GuestSockets::IsOpen(descriptor)) return false; + errno = GUEST_ESPIPE; return true; } @@ -388,7 +419,7 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { constexpr long PipeFilesystem = 0x50495045; struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; - if (S_ISSOCK(file.st_mode)) return GUEST_EINVAL; + if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; if (S_ISFIFO(file.st_mode)) { struct statfs kind{}; if (::fstatfs(descriptor, &kind) == 0 && static_cast(kind.f_type) == PipeFilesystem) return GUEST_EINVAL; @@ -436,10 +467,6 @@ static int PathError(const char* path) { bool DescriptorIsOpen_nid_no_patch(int descriptor) { return descriptor >= 0 && WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor) == -1 ? -1 : 0; }) == 0; } -static void RejectDirectoryDuplicate(int descriptor, const char* function) { - if (File::DirectoryDescriptorPath(descriptor)) - throw std::runtime_error(std::string(function) + ": duplicating a directory descriptor is not supported on Windows"); -} #else bool DescriptorIsOpen_nid_no_patch(int descriptor) { return descriptor >= 0 && ::fcntl(descriptor, F_GETFD) >= 0; @@ -473,14 +500,39 @@ int File::DuplicateFrom(int fd, int start) { #else const int duplicate = ::fcntl(fd, F_DUPFD, start); if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); - if (duplicate >= GuestSockets::FirstDescriptor) - throw std::runtime_error("fcntl: host descriptor reached the guest socket range"); if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); File::ShareDescriptor(fd, duplicate); return duplicate; #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) { @@ -491,16 +543,30 @@ 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) { - if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; *__error_nid_postfix() = socketError; return -1; } + if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); + EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); + ForgetJitSharedMemory(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -524,15 +590,13 @@ void PreserveHostStandardError() { #ifdef _WIN32 const int saved = WithoutParameterHandler([] { return ::_dup(2); }); #else - const int saved = ::dup(2); + const int saved = KeepHostStandardError_nid_no_patch(); #endif if (saved < 0) throw std::runtime_error("dup2: cannot keep the host standard error"); int expected = -1; if (!g_hostStandardError.compare_exchange_strong(expected, saved)) { #ifdef _WIN32 _close(saved); -#else - ::close(saved); #endif } } @@ -545,45 +609,55 @@ int File::HostStandardError() { extern "C" { int APS5_VABI dup_nid_postfix(int d) { - if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Duplicate(d); + if (GuestSockets::IsOpen(d)) return GuestSockets::Duplicate(d); if (d < 0) return PosixFailure(GUEST_EBADF); + if (IsKqueueDescriptor(d)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); #ifdef _WIN32 - RejectDirectoryDuplicate(d, __func__); const int duplicate = WithoutParameterHandler([d] { return ::_dup(d); }); + if (duplicate >= 0) File::DuplicateDirectoryDescriptor(d, duplicate); #else const int duplicate = ::dup(d); #endif if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); - if (duplicate >= GuestSockets::FirstDescriptor) - throw std::runtime_error(std::string(__func__) + ": host descriptor reached the guest socket range"); if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); File::ShareDescriptor(d, duplicate); + DuplicateJitSharedMemory(d, duplicate); return duplicate; } int APS5_VABI dup2_nid_postfix(int from, int to) { if (from < 0 || to < 0) return PosixFailure(GUEST_EBADF); - const bool socketFrom = from >= GuestSockets::FirstDescriptor; - const bool socketTo = to >= GuestSockets::FirstDescriptor; - if (socketFrom && socketTo) return GuestSockets::DuplicateTo(from, to); - if (socketFrom) { - if (!GuestSockets::IsOpen(from)) return PosixFailure(GUEST_EBADF); - throw std::runtime_error(std::string(__func__) + ": moving a guest socket onto host descriptor " + std::to_string(to) + " is not supported"); + if (GuestSockets::IsOpen(from)) { + if (from != to && to <= 2) + throw std::runtime_error(std::string(__func__) + ": moving a guest socket onto standard descriptor " + std::to_string(to) + " is not supported"); + if (GuestSockets::DuplicateTo(from, to) < 0) return -1; + if (from == to) return to; + File::ForgetRandomDevice(to); +#ifdef _WIN32 + File::ForgetDirectoryDescriptor(to); + File::ForgetFileLock(to); +#endif + return to; } - if (socketTo) - throw std::runtime_error(std::string(__func__) + ": moving host descriptor " + std::to_string(from) + " into the guest socket range is not supported"); const bool sourceIsRandom = File::IsRandomDevice(from); + if (from != to && IsKqueueDescriptor(from)) throw std::runtime_error(std::string(__func__) + ": duplicating a kqueue descriptor is not supported"); #ifdef _WIN32 if (WithoutParameterHandler([from] { return ::_get_osfhandle(from) == -1 ? -1 : 0; }) != 0) return PosixFailure(GUEST_EBADF); if (from == to) return to; - RejectDirectoryDuplicate(from, __func__); if (to == 2) PreserveHostStandardError(); - if (WithoutParameterHandler([from, to] { return ::_dup2(from, to); }) != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + auto* const standard = HostStandardHandle_nid_no_patch(to); + const int duplicated = WithoutParameterHandler([from, to] { return ::_dup2(from, to); }); + KeepStandardHandle_nid_no_patch(to, standard); + if (duplicated != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + EqueueDescriptorClosed(to); + GuestSockets::Forget(to); File::ForgetDirectoryDescriptor(to); + File::DuplicateDirectoryDescriptor(from, to); File::ForgetFileLock(to); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); File::ShareDescriptor(from, to); + DuplicateJitSharedMemory(from, to); return to; #else if (from == to) { @@ -593,14 +667,18 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (to == 2) PreserveHostStandardError(); const int result = ::dup2(from, to); if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + EqueueDescriptorClosed(to); + GuestSockets::Forget(to); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); File::ShareDescriptor(from, to); + DuplicateJitSharedMemory(from, to); return result; #endif } int APS5_VABI flock_nid_postfix(int d, int operation) { + if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); if (NativeFlock(d, type | (operation & 4)) != 0) { @@ -618,7 +696,7 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { } int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { - if (d >= GuestSockets::FirstDescriptor) return PosixFailure(GuestSockets::IsOpen(d) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); KernelStatfs result{}; result.f_version = KERNEL_STATFS_VERSION; @@ -634,12 +712,13 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (sb == nullptr) { APS5_INVALID_ARG_EX; } - File::FillFileStat(d, sb); + if (GuestSockets::IsOpen(d)) throw std::runtime_error(std::string(__func__) + ": the status of a guest socket is not implemented"); + if (!File::FillFileStatFromDescriptor(d, sb)) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); return 0; } int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { - if (length < 0) { + if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); } #ifdef _WIN32 @@ -717,6 +796,7 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off 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)); } @@ -733,14 +813,14 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_ } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { - if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Read(d, buf, static_cast(nbytes)); + if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; return PosixResult(sceKernelRead(d, buf == nullptr ? &emptyBuffer : buf, static_cast(nbytes))); } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { - if (descriptor >= GuestSockets::FirstDescriptor) return GuestSockets::Read(descriptor, buffer, count); + if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; return PosixResult(sceKernelRead(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); @@ -750,14 +830,14 @@ int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { if (size < 0) { APS5_INVALID_ARG_EX; } - if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Write(d, str, static_cast(size)); + if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); if (str == nullptr && size != 0) return PosixFailure(GUEST_EFAULT); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(d, str == nullptr ? &emptyBuffer : str, static_cast(size))); } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { - if (descriptor >= GuestSockets::FirstDescriptor) return GuestSockets::Write(descriptor, buffer, count); + if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); const char emptyBuffer = 0; return PosixResult(sceKernelWrite(descriptor, buffer == nullptr ? &emptyBuffer : buffer, count)); @@ -789,18 +869,20 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; 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; return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } 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 (!File::FillFileStatFromDescriptor(d, sb)) return SceErrorFromErrno(errno); return 0; } int APS5_VABI sceKernelFsync(int fd) { - if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -810,7 +892,7 @@ int APS5_VABI sceKernelFsync(int fd) { } int APS5_VABI sceKernelFdatasync(int fd) { - if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -831,6 +913,7 @@ int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { #ifdef _WIN32 int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); if (basep != nullptr) *basep = 0; @@ -844,6 +927,7 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { #else int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); constexpr std::size_t GuestHeaderBytes = 8; constexpr std::size_t GuestMaxName = 255; if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); @@ -974,7 +1058,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co } std::deque destinations; if (!write && !OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); - if (offset == nullptr && d >= GuestSockets::FirstDescriptor) { + if (offset == nullptr && GuestSockets::IsOpen(d)) { const auto result = write ? GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt) : GuestSockets::Readv(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; @@ -1042,7 +1126,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { if (const int error = CheckIovecs(iov, iovcnt)) return error; std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); - if (d >= GuestSockets::FirstDescriptor) { + if (GuestSockets::IsOpen(d)) { const auto result = GuestSockets::Readv(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } @@ -1053,7 +1137,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { if (const int error = CheckIovecs(iov, iovcnt)) return error; - if (d >= GuestSockets::FirstDescriptor) { + if (GuestSockets::IsOpen(d)) { const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } @@ -1102,10 +1186,16 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { std::filesystem::path source; if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); std::error_code error; - if (!std::filesystem::exists(source, error)) return SceErrorFromErrno(GUEST_ENOENT); + const auto sourceStatus = std::filesystem::status(source, error); + if (!std::filesystem::exists(sourceStatus)) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path destination; if (const int result = ResolvePathError_nid_no_patch(to, destination)) return SceKernelError(result); - std::filesystem::rename(source, destination, error); + bool replacesFile = false; +#ifdef _WIN32 + const auto destinationStatus = std::filesystem::symlink_status(destination, error); + replacesFile = !std::filesystem::is_directory(sourceStatus) && std::filesystem::exists(destinationStatus) && !std::filesystem::is_directory(destinationStatus); +#endif + error = RenameNative_nid_no_patch(source, destination, replacesFile); if (error) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(source); RecordWrittenPath_nid_no_patch(destination); @@ -1149,7 +1239,7 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { } int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { - if (d >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(d) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1197,8 +1287,21 @@ int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { return PosixResult(sceKernelUtimes_nid_postfix(path, times)); } +int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (const int error = PathError(path)) return PosixFailure(error); + if (times != nullptr) { + for (int i = 0; i < 2; ++i) { + if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return PosixFailure(GUEST_EINVAL); + } + } + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (NativeUtimes(native, times, false) != 0) return PosixResult(SceErrorFromErrno(errno)); + return 0; +} + int APS5_VABI sceKernelFutimes(int d, const KernelTimeval* times) { - if (d >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(d) ? GUEST_EINVAL : GUEST_EBADF); + if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); if (times != nullptr) { for (int i = 0; i < 2; ++i) { if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SceErrorFromErrno(GUEST_EINVAL); diff --git a/core/libs/prx/libkernel/File/src/Terminal.cpp b/core/libs/prx/libkernel/File/src/Terminal.cpp index f013a865fb..648c63eb7c 100644 --- a/core/libs/prx/libkernel/File/src/Terminal.cpp +++ b/core/libs/prx/libkernel/File/src/Terminal.cpp @@ -7,6 +7,7 @@ #include #else #include +#include #include #endif @@ -27,6 +28,12 @@ constexpr int SetDrain = 1; constexpr int SetFlush = 2; constexpr int SetSoft = 0x10; constexpr int InternetFamily = 2; +constexpr int InputQueue = 1; +constexpr int OutputQueue = 2; +constexpr int BothQueues = 3; +constexpr int OutputOff = 1; +constexpr int InputOn = 4; +constexpr int StandardStreams = 3; int Fail(int error) { *__error_nid_postfix() = error; @@ -35,25 +42,34 @@ int Fail(int error) { #ifdef _WIN32 void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -bool HostOpen(int descriptor) { +HANDLE HostHandle(int descriptor) { const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); const auto handle = ::_get_osfhandle(descriptor); _set_thread_local_invalid_parameter_handler(previous); - return reinterpret_cast(handle) != INVALID_HANDLE_VALUE; + return reinterpret_cast(handle); } +bool HostOpen(int descriptor) { return HostHandle(descriptor) != INVALID_HANDLE_VALUE; } +bool HostPipe(int descriptor) { return ::GetFileType(HostHandle(descriptor)) == FILE_TYPE_PIPE; } #else bool HostOpen(int descriptor) { return ::fcntl(descriptor, F_GETFD) != -1; } +bool HostPipe(int descriptor) { + struct stat status{}; + return ::fstat(descriptor, &status) == 0 && S_ISFIFO(status.st_mode); +} #endif int TerminalError(int descriptor) { - if (descriptor >= GuestSockets::FirstDescriptor) { - const int family = GuestSockets::Family(descriptor); - if (family < 0) return BadDescriptor; - return family == InternetFamily ? AddressNotAvailable : OperationNotSupported; - } + const int family = GuestSockets::Family(descriptor); + if (family >= 0) return family == InternetFamily ? AddressNotAvailable : OperationNotSupported; if (descriptor < 0 || !HostOpen(descriptor)) return BadDescriptor; return NotTerminal; } + +int ProcessGroupError(int descriptor, const char* pipeMessage) { + const int error = TerminalError(descriptor); + if (error == NotTerminal && descriptor >= StandardStreams && HostPipe(descriptor)) throw std::runtime_error(pipeMessage); + return error; +} } extern "C" { @@ -69,4 +85,30 @@ int APS5_VABI tcsetattr_nid_postfix(int descriptor, int action, const void* attr if (attributes == nullptr) return Fail(BadAddress); return Fail(TerminalError(descriptor)); } + +int APS5_VABI tcsendbreak_nid_postfix(int descriptor, int duration) { + static_cast(duration); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcdrain_nid_postfix(int descriptor) { return Fail(TerminalError(descriptor)); } + +int APS5_VABI tcflush_nid_postfix(int descriptor, int queue) { + if (queue != InputQueue && queue != OutputQueue && queue != BothQueues) return Fail(InvalidArgument); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcflow_nid_postfix(int descriptor, int action) { + if (action < OutputOff || action > InputOn) return Fail(InvalidArgument); + return Fail(TerminalError(descriptor)); +} + +int APS5_VABI tcgetpgrp_nid_postfix(int descriptor) { + return Fail(ProcessGroupError(descriptor, "tcgetpgrp: the SIGIO owner of a pipe is not supported")); +} + +int APS5_VABI tcsetpgrp_nid_postfix(int descriptor, int group) { + static_cast(group); + return Fail(ProcessGroupError(descriptor, "tcsetpgrp: the SIGIO owner of a pipe is not supported")); +} } diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 769bf79cae..1dc372c387 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -162,13 +162,6 @@ static void InitializeDeferredModule(HMODULE native) { } #endif -static std::filesystem::path RelinkedModulePath(const std::filesystem::path& path) { - auto relinked = path; - relinked += ".guest.prx"; - std::error_code error; - return std::filesystem::is_regular_file(relinked, error) ? relinked : path; -} - void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { if ((flags & ~0x103) || (flags & 3) == 0 || (flags & 3) == 3) { Error("dlopen: unsupported flags"); return nullptr; @@ -182,7 +175,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { module->owned = false; } else { if (!*path) { Error("dlopen: empty module path"); return nullptr; } - const auto resolved = RelinkedModulePath(ResolvePath_nid_no_patch(path)); + const auto resolved = RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)); module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); if (module->native) InitializeDeferredModule(static_cast(module->native)); } @@ -192,7 +185,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { Error(message); return nullptr; } #else - const auto resolved = path ? RelinkedModulePath(ResolvePath_nid_no_patch(path)).string() : std::string{}; + const auto resolved = path ? RelinkedModulePath_nid_no_patch(ResolvePath_nid_no_patch(path)).string() : std::string{}; const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index f1d39a1ecd..d196d86131 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -241,11 +241,4 @@ int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* ad return 0; } -// unknown signature -std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 378333b50a..63451dbcad 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -428,6 +428,35 @@ int APS5_VABI sceKernelGetModuleInfo2(KernelModule handle, ModuleInfo* info) { return FillModuleInfo(handle, info, true); } +std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void** address, std::size_t* size) { + if (!address || !size) throw std::invalid_argument("sceKernelGetLibkernelTextLocation: null argument"); + const auto images = GuestImages(); + if (images.empty()) throw std::runtime_error("sceKernelGetLibkernelTextLocation: the main program is not loaded"); + ModuleInfo info{}; +#ifdef _WIN32 + const bool found = Fill(images.front().address, info); +#else + ImageSearch search{images.front().address, &info, false}; + dl_iterate_phdr(FindImage, &search); + const bool found = search.found; +#endif + if (!found || info.segment_count == 0) + throw std::runtime_error("sceKernelGetLibkernelTextLocation: the main program has no segments"); + *address = reinterpret_cast(static_cast(info.segments[0].address)); + *size = info.segments[0].size; + return 0; +} + +std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo* result) { + if (!result) throw std::invalid_argument("sceKernelInternalMemoryGetModuleSegmentInfo: result is null"); + void* address = nullptr; + std::size_t size = 0; + const auto status = sceKernelGetLibkernelTextLocation_nid_postfix(&address, &size); + if (status != 0) return status; + *result = {reinterpret_cast(address), size}; + return 0; +} + std::uint64_t GuestImageSegmentBytes_nid_no_patch() { return GuestSegmentBytes(); } 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 37beb11416..90ad038f81 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -29,6 +29,12 @@ int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); } static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_RR = 3; + +static bool AcceptsSchedParam(int policy, int priority) { + if (policy != GUEST_SCHED_FIFO && policy != GUEST_SCHED_RR) return false; + return (priority >= 256 && priority <= 767) || (priority >= 896 && priority <= 959); +} static constexpr std::size_t GUEST_CPUSET_BYTES = 32; static constexpr int GUEST_ERANGE = 34; @@ -41,25 +47,32 @@ int APS5_VABI getpid_nid_postfix(void); int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask) { constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; - constexpr int GuestEfault = 14, GuestErange = 34; + constexpr int GuestEsrch = 3, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; - if (size < sizeof(KernelCpumask) || size > MaximumSize) { - *__error_nid_postfix() = GuestErange; - return -1; - } - const bool thread = level == LevelWhich && which == WhichThread && id == -1; - const bool process = level == LevelWhich && which == WhichProcess && (id == -1 || id == getpid_nid_postfix()); - if (!thread && !process) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); - if (!mask) { - *__error_nid_postfix() = GuestEfault; + const auto fail = [](int error) { + *__error_nid_postfix() = error; return -1; + }; + if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); + if (!mask) return fail(GuestEfault); + auto* self = scePthreadSelf(); + if (which == WhichThread) { + if (id != -1 && id != static_cast(self->tid)) { + if (LiveGuestThreadExists(id)) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); + return fail(GuestEsrch); + } + } else if (which == WhichProcess) { + if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); + } else if (which >= 3 && which <= 6) { + NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); + } else { + return fail(GuestEinval); } + if (level < 1 || level > 3) return fail(GuestEinval); + if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); KernelCpumask affinity = 0; - if (thread) scePthreadGetaffinity(scePthreadSelf(), &affinity); - else { - scePthreadSelf(); - affinity = LiveGuestThreadsAffinity(); - } + if (which == WhichThread) scePthreadGetaffinity(self, &affinity); + else affinity = LiveGuestThreadsAffinity(); std::memset(mask, 0, size); std::memcpy(mask, &affinity, sizeof(affinity)); return 0; @@ -67,7 +80,7 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, const void* mask) { constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; - constexpr int GuestEdeadlk = 11, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; + constexpr int GuestEsrch = 3, GuestEdeadlk = 11, GuestEfault = 14, GuestEinval = 22, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; const auto fail = [](int error) { *__error_nid_postfix() = error; @@ -79,14 +92,26 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t for (std::size_t index = sizeof(KernelCpumask); index < size; ++index) { if (bytes[index] != 0) return fail(GuestEinval); } - const bool thread = level == LevelWhich && which == WhichThread && id == -1; - const bool process = level == LevelWhich && which == WhichProcess && (id == -1 || id == getpid_nid_postfix()); - if (!thread && !process) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); + auto* self = scePthreadSelf(); + if (which == WhichThread) { + if (id != -1 && id != static_cast(self->tid)) { + if (LiveGuestThreadExists(id)) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); + return fail(GuestEsrch); + } + } else if (which == WhichProcess) { + if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); + } else if (which >= 3 && which <= 6) { + NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); + } else { + return fail(GuestEinval); + } + if (level < 1 || level > 3) return fail(GuestEinval); + if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); KernelCpumask requested = 0; std::memcpy(&requested, bytes, sizeof(requested)); const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); if (affinity == 0) return fail(GuestEdeadlk); - if (thread) scePthreadSetaffinity(scePthreadSelf(), affinity); + if (which == WhichThread) scePthreadSetaffinity(self, affinity); else SetLiveGuestThreadsAffinity(affinity); return 0; } @@ -109,20 +134,21 @@ void APS5_VABI pthread_exit_nid_postfix(void* value) { int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return SCE_KERNEL_ERROR_EINVAL; - *policy = GUEST_SCHED_FIFO; - return scePthreadGetprio(thread, ¶m->sched_priority); + const int result = scePthreadGetprio(thread, ¶m->sched_priority); + if (result == 0) *policy = thread->schedPolicy.load(std::memory_order_relaxed); + return result; } int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param) { - (void)policy; - if (!param) return SCE_KERNEL_ERROR_EINVAL; - return scePthreadSetprio(thread, param->sched_priority); + if (!param || !AcceptsSchedParam(policy, param->sched_priority)) return SCE_KERNEL_ERROR_EINVAL; + const int result = scePthreadSetprio(thread, param->sched_priority); + if (result == 0) thread->schedPolicy.store(policy, std::memory_order_relaxed); + return result; } int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return PosixThread::GUEST_EINVAL; - *policy = GUEST_SCHED_FIFO; - return PosixThread::ToErrno(scePthreadGetprio(thread, ¶m->sched_priority)); + return PosixThread::ToErrno(scePthreadGetschedparam(thread, policy, param)); } int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset) { @@ -137,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)); } @@ -200,9 +241,8 @@ int APS5_VABI pthread_setprio_nid_postfix(Pthread thread, int prio) { } int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param) { - (void)policy; if (!param) return PosixThread::GUEST_EINVAL; - return PosixThread::ToErrno(scePthreadSetprio(thread, param->sched_priority)); + return PosixThread::ToErrno(scePthreadSetschedparam(thread, policy, param)); } void APS5_VABI pthread_testcancel_nid_postfix(void) { 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/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 5ddf36a9fc..e22426757f 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #ifndef _WIN32 @@ -50,15 +49,11 @@ struct PthreadRwlockattrPrivate { }; struct PthreadRwlockPrivate { - // TODO(technical debt): winpthreads initializes a static rwlock on first use and fails a - // concurrent first lock with EINVAL, which shared_timed_mutex ignores. Initialize it here. - PthreadRwlockPrivate() { - _lock.lock(); - _lock.unlock(); - } - std::shared_timed_mutex _lock; + std::mutex _state; + TimedWait::Condition _changed; std::atomic _writer; std::atomic _readers{0}; + int _waitingWriters = 0; }; struct PthreadCondattrPrivate { @@ -122,6 +117,7 @@ struct PthreadPrivate { std::atomic references{2}; std::atomic inWait{false}; std::atomic pendingException{0}; + std::atomic schedPolicy{1}; void* wakeEvent = nullptr; void* stackAddress = nullptr; std::size_t stackSize = 0; @@ -133,6 +129,9 @@ struct PthreadPrivate { #ifndef _WIN32 clockid_t hostCpuClock{}; bool hostCpuClockBound = false; + void* guestStack = nullptr; + std::size_t guestStackBytes = 0; + int hostTid = 0; #endif std::mutex nameLock; std::string name; @@ -153,10 +152,13 @@ struct PthreadPrivate { }; bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end); void NoteGuestStackProtection(std::uintptr_t start, std::size_t bytes, bool readWrite); int GuestThreadCpuClockId(const PthreadPrivate* thread); bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); +bool GuestThreadIsLive(const PthreadPrivate* thread); +bool LiveGuestThreadExists(std::int64_t tid); #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 3ff9b30885..a77cd365f3 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -35,6 +35,27 @@ static bool OwnsWrite(const PthreadRwlockPrivate* lock) { return lock->_writer.load(std::memory_order_acquire) == std::this_thread::get_id(); } +static thread_local int readLocksHeld = 0; + +static bool ReaderMayEnter(const PthreadRwlockPrivate* lock) { + return lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && (lock->_waitingWriters == 0 || readLocksHeld != 0); +} + +static bool WriterMayEnter(const PthreadRwlockPrivate* lock) { + return lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && lock->_readers.load(std::memory_order_acquire) == 0; +} + +static int EnterRead(PthreadRwlockPrivate* lock) { + lock->_readers.fetch_add(1, std::memory_order_release); + ++readLocksHeld; + return SCE_OK; +} + +static int EnterWrite(PthreadRwlockPrivate* lock) { + lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); + return SCE_OK; +} + extern "C" { int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock) { @@ -65,79 +86,77 @@ int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; -#ifdef _WIN32 - while (!lock->_lock.try_lock_shared()) TimedWait::SleepNanos(1000000); -#else - lock->_lock.lock_shared(); -#endif - lock->_readers.fetch_add(1, std::memory_order_release); - return SCE_OK; + std::unique_lock state(lock->_state); + lock->_changed.Wait(state, [&] { return ReaderMayEnter(lock); }); + return EnterRead(lock); } int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; - if (!lock->_lock.try_lock_shared()) return SCE_KERNEL_ERROR_EBUSY; - lock->_readers.fetch_add(1, std::memory_order_release); - return SCE_OK; + std::lock_guard state(lock->_state); + return ReaderMayEnter(lock) ? EnterRead(lock) : SCE_KERNEL_ERROR_EBUSY; } int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); - if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; - lock->_readers.fetch_add(1, std::memory_order_release); - return SCE_OK; + const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + std::unique_lock state(lock->_state); + if (!lock->_changed.WaitUntil(state, deadline, [&] { return ReaderMayEnter(lock); })) return SCE_KERNEL_ERROR_ETIMEDOUT; + return EnterRead(lock); } int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; -#ifdef _WIN32 - while (!lock->_lock.try_lock()) TimedWait::SleepNanos(1000000); -#else - lock->_lock.lock(); -#endif - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + std::unique_lock state(lock->_state); + ++lock->_waitingWriters; + lock->_changed.Wait(state, [&] { return WriterMayEnter(lock); }); + --lock->_waitingWriters; + return EnterWrite(lock); } int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; - if (OwnsWrite(lock) || !lock->_lock.try_lock()) return SCE_KERNEL_ERROR_EBUSY; - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; + std::lock_guard state(lock->_state); + return WriterMayEnter(lock) ? EnterWrite(lock) : SCE_KERNEL_ERROR_EBUSY; } int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); - if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; - lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); - return SCE_OK; + const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); + std::unique_lock state(lock->_state); + ++lock->_waitingWriters; + const bool acquired = lock->_changed.WaitUntil(state, deadline, [&] { return WriterMayEnter(lock); }); + --lock->_waitingWriters; + if (acquired) return EnterWrite(lock); + state.unlock(); + lock->_changed.NotifyAll(); + return SCE_KERNEL_ERROR_ETIMEDOUT; } int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); if (!lock) return SCE_KERNEL_ERROR_EINVAL; + std::unique_lock state(lock->_state); if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); - lock->_lock.unlock(); - return SCE_OK; + } else { + if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{}) return SCE_KERNEL_ERROR_EPERM; + if (lock->_readers.load(std::memory_order_acquire) == 0) return SCE_KERNEL_ERROR_EPERM; + if (readLocksHeld != 0) --readLocksHeld; + if (lock->_readers.fetch_sub(1, std::memory_order_acq_rel) != 1) return SCE_OK; } - if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{}) return SCE_KERNEL_ERROR_EPERM; - int readers = lock->_readers.load(std::memory_order_acquire); - do { - if (readers == 0) return SCE_KERNEL_ERROR_EPERM; - } while (!lock->_readers.compare_exchange_weak(readers, readers - 1, std::memory_order_acq_rel, std::memory_order_acquire)); - lock->_lock.unlock_shared(); + state.unlock(); + lock->_changed.NotifyAll(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Sem.cpp b/core/libs/prx/libkernel/Pthread/src/Sem.cpp index 8255d87b9f..ea9006380e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Sem.cpp @@ -1,6 +1,7 @@ #include "../include/Pthread.hpp" #include "../include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -11,6 +12,7 @@ namespace { constexpr int sceBusy = static_cast(0x80020010u); constexpr int sceTimedOut = static_cast(0x8002003cu); +constexpr int sceSemValueMax = 0x7fffffff; std::mutex semInitializationMutex; PthreadSem destroyedSem() { @@ -34,6 +36,8 @@ extern "C" { int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name) { if (!sem) throw std::invalid_argument("Semaphore pointer is null"); + if (value > static_cast(sceSemValueMax)) + return SCE_KERNEL_ERROR_EINVAL; (void)flag; (void)name; auto replacement = std::make_unique(value); @@ -58,6 +62,8 @@ int APS5_VABI scePthreadSemPost(PthreadSem* sem) { auto* current = resolveSem(sem); { std::lock_guard lock(current->_mutex); + if (current->_count == sceSemValueMax) + return SCE_KERNEL_ERROR_EOVERFLOW; ++current->_count; } current->_cv.NotifyOne(); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 34e9ea435a..cdbf5c9ab7 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/CpuTopology.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -73,6 +75,101 @@ static GuestThreadState& CurrentGuestState() { static std::mutex stackLock; static std::map> liveStacks; static std::map protectedStackPages; +#if defined(__linux__) +struct GuestStackMapping { + std::uintptr_t mappingEnd; + std::uintptr_t stackStart; + std::uintptr_t stackEnd; +}; +static std::mutex guestStackMappingsLock; +static std::map guestStackMappings; + +static void RegisterGuestStackMapping(PthreadPrivate* thread) { + const auto mappingStart = reinterpret_cast(thread->guestStack); + const auto stackStart = reinterpret_cast(thread->stackAddress); + std::lock_guard lock(guestStackMappingsLock); + guestStackMappings[mappingStart] = {mappingStart + thread->guestStackBytes, stackStart, stackStart + thread->stackSize}; +} + +static void UnregisterGuestStackMapping(void* mapping) { + std::lock_guard lock(guestStackMappingsLock); + guestStackMappings.erase(reinterpret_cast(mapping)); +} +#endif + +#if defined(__linux__) +static constexpr std::size_t GUEST_STACK_GUARD = PS5_PAGE_SIZE; + +struct RetiredStack { + void* address; + std::size_t bytes; + int hostTid; +}; + +static std::mutex retiredStacksLock; +static std::vector retiredStacks; + +static void AllocateGuestStack(PthreadPrivate* thread, std::size_t usableBytes) { + const std::size_t bytes = (usableBytes + PS5_PAGE_SIZE - 1) / PS5_PAGE_SIZE * PS5_PAGE_SIZE + GUEST_STACK_GUARD; + void* stack = nullptr; + if (DoMapAnon(&stack, bytes, 3, 0) != 0) + throw std::runtime_error("scePthreadCreate: cannot map the guest thread stack"); + if (DoMprotect(stack, GUEST_STACK_GUARD, 0) != 0) { + DoMunmap(stack, bytes); + throw std::runtime_error("scePthreadCreate: cannot protect the guest thread stack guard"); + } + thread->guestStack = stack; + thread->guestStackBytes = bytes; +} + +static void FreeGuestStack(PthreadPrivate* thread) { + if (thread->guestStack == nullptr) return; + void* const mapping = thread->guestStack; + DoMunmap(thread->guestStack, thread->guestStackBytes); + UnregisterGuestStackMapping(mapping); + thread->guestStack = nullptr; + thread->guestStackBytes = 0; +} + +static bool HostThreadGone(int hostTid) { + if (hostTid == 0) return false; + char path[48]; + std::snprintf(path, sizeof(path), "/proc/self/task/%d", hostTid); + return access(path, F_OK) != 0; +} + +static void ReapRetiredStacks() { + std::vector ready; + { + std::lock_guard lock(retiredStacksLock); + for (auto it = retiredStacks.begin(); it != retiredStacks.end();) { + if (HostThreadGone(it->hostTid)) { + ready.push_back(*it); + it = retiredStacks.erase(it); + } else { + ++it; + } + } + } + for (const auto& stack : ready) { + DoMunmap(stack.address, stack.bytes); + UnregisterGuestStackMapping(stack.address); + } +} + +static void RetireGuestStack(PthreadPrivate* thread) { + if (thread->guestStack == nullptr) return; + { + std::lock_guard lock(retiredStacksLock); + retiredStacks.push_back({thread->guestStack, thread->guestStackBytes, thread->hostTid}); + } + thread->guestStack = nullptr; + thread->guestStackBytes = 0; +} +#else +static void FreeGuestStack(PthreadPrivate*) {} +static void RetireGuestStack(PthreadPrivate*) {} +#endif static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 @@ -176,6 +273,24 @@ bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintpt return false; } +bool GuestThreadStackHostRangeOverlaps(std::uintptr_t start, std::uintptr_t end) { +#if defined(__linux__) + std::lock_guard lock(guestStackMappingsLock); + auto it = guestStackMappings.upper_bound(start); + if (it != guestStackMappings.begin()) --it; + for (; it != guestStackMappings.end() && it->first < end; ++it) { + const auto mappingStart = it->first; + const auto& stack = it->second; + if (start < stack.stackStart && end > mappingStart) return true; + if (start < stack.mappingEnd && end > stack.stackEnd) return true; + } +#else + (void)start; + (void)end; +#endif + return false; +} + struct CpuClockRegistry { std::mutex lock; std::map threads; @@ -194,6 +309,9 @@ PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false const auto* parent = CurrentGuestState().current; const KernelCpumask inherited = parent ? parent->affinity.load(std::memory_order_relaxed) : registry.processAffinity; affinity.store(attr && attr->_affinityExplicit ? attr->_affinity : inherited, std::memory_order_relaxed); + constexpr int ExplicitSched = 0; + const int inheritedPolicy = parent ? parent->schedPolicy.load(std::memory_order_relaxed) : schedPolicy.load(std::memory_order_relaxed); + schedPolicy.store(attr && attr->_inheritsched == ExplicitSched ? attr->_schedpolicy : inheritedPolicy, std::memory_order_relaxed); if (registry.nextThread > static_cast(CPU_CLOCK_ID_MASK)) throw std::runtime_error("Guest thread CPU clock identifiers are exhausted"); cpuClockThread = registry.nextThread++; @@ -244,6 +362,25 @@ KernelCpumask LiveGuestThreadsAffinity() { return mask; } +bool GuestThreadIsLive(const PthreadPrivate* thread) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, live] : registry.threads) { + if (live != thread) continue; + return !live->_finished.load(std::memory_order_acquire); + } + return false; +} + +bool LiveGuestThreadExists(std::int64_t tid) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire) && thread->tid == tid) return true; + } + return false; +} + bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -505,6 +642,10 @@ static void* StartNativeThread(void* opaque) { throw std::runtime_error("Cannot query guest thread stack"); self->stackAddress = reinterpret_cast(high - reserve - self->stackSize); self->threadId = std::this_thread::get_id(); +#if defined(__linux__) + self->hostTid = static_cast(gettid()); + RegisterGuestStackMapping(self); +#endif args->initialized.set_value(); } catch (...) { args->initialized.set_exception(std::current_exception()); @@ -588,12 +729,22 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread sigset_t creatorMask; pthread_sigmask(SIG_SETMASK, nullptr, &creatorMask); args->signalMask = creatorMask; +#if defined(__linux__) + ReapRetiredStacks(); + AllocateGuestStack(p.get(), nativeStack); +#endif auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); auto initialized = native->initialized.get_future(); pthread_attr_t nativeAttr; - if (const int result = pthread_attr_init(&nativeAttr); result != 0) + if (const int result = pthread_attr_init(&nativeAttr); result != 0) { + FreeGuestStack(p.get()); throw std::system_error(result, std::generic_category(), "Creating guest thread"); + } +#if defined(__linux__) + int created = pthread_attr_setstack(&nativeAttr, static_cast(p->guestStack) + GUEST_STACK_GUARD, p->guestStackBytes - GUEST_STACK_GUARD); +#else int created = pthread_attr_setstacksize(&nativeAttr, nativeStack); +#endif if (created == 0) { sigset_t allSignals; sigfillset(&allSignals); @@ -602,18 +753,22 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread pthread_sigmask(SIG_SETMASK, &creatorMask, nullptr); } pthread_attr_destroy(&nativeAttr); - if (created != 0) + if (created != 0) { + FreeGuestStack(p.get()); throw std::system_error(created, std::generic_category(), "Creating guest thread"); + } native.release(); try { initialized.get(); if (detached) { if (const int result = pthread_detach(p->hostThread); result != 0) throw std::system_error(result, std::generic_category(), "Detaching guest thread"); + RetireGuestStack(p.get()); } } catch (...) { start.set_value(false); pthread_join(p->hostThread, nullptr); + FreeGuestStack(p.get()); throw; } auto* published = p.release(); @@ -642,6 +797,7 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { #else if (const int result = pthread_join(thread->hostThread, nullptr); result != 0) throw std::system_error(result, std::generic_category(), "Joining guest thread"); + FreeGuestStack(thread); if (retval) *retval = thread->_retval; ReleaseThread(thread); #endif @@ -657,6 +813,7 @@ int APS5_VABI scePthreadDetach(Pthread thread) { #else if (const int result = pthread_detach(thread->hostThread); result != 0) throw std::system_error(result, std::generic_category(), "Detaching guest thread"); + RetireGuestStack(thread); ReleaseThread(thread); #endif return SCE_OK; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index c990c66866..622a17bd20 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -25,3 +25,5 @@ extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer clos extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); +extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 41bf8caf38..103f2d0684 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -2,8 +2,8 @@ #include #include #include +#include "prx/libkernel/Socket/include/SocketPoll.hpp" namespace GuestSockets { -constexpr int FirstDescriptor = 0x10000000; struct Iovec { void* base; std::size_t length; @@ -11,11 +11,13 @@ struct Iovec { int Duplicate(int descriptor); int DuplicateTo(int descriptor, int target); int Close(int descriptor); +void Forget(int descriptor); 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); bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof); +int Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds); 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 f92f2d0136..5082e538ee 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -47,6 +47,16 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +struct PollGroup { + KernelSocketPoll::Poller poll; + std::vector entries; + std::vector slots; +}; + +int KernelSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { + return GuestSockets::Poll(entries, count, timeoutMilliseconds); +} + #ifndef _WIN32 short HostEventFlags(short events) { @@ -65,20 +75,17 @@ short KernelEventFlags(short revents) { return flags; } -int HostPoll(KernelSocketPoll::Entry* descriptors, std::size_t count, int timeoutMilliseconds) { - std::vector native(count); - for (std::size_t index = 0; index < count; ++index) { +int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMilliseconds) { + std::vector native(static_cast(count)); + for (std::size_t index = 0; index < native.size(); ++index) { native[index].fd = descriptors[index].descriptor; - native[index].events = descriptors[index].events; + native[index].events = HostEventFlags(descriptors[index].events); native[index].revents = 0; } - const int result = ::poll(native.data(), static_cast(count), timeoutMilliseconds); - if (result < 0) return -1; - for (std::size_t index = 0; index < count; ++index) { - if ((native[index].revents & POLLNVAL) != 0) { - errno = EBADF; - return -1; - } + const int result = ::poll(native.data(), static_cast(native.size()), timeoutMilliseconds); + if (result < 0) return -errno; + for (std::size_t index = 0; index < native.size(); ++index) { + if ((native[index].revents & POLLNVAL) != 0) return -EBADF; descriptors[index].revents = KernelEventFlags(native[index].revents); } return result; @@ -131,10 +138,8 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* *__error_nid_postfix() = GuestEinval; return -1; } - std::vector sockets; - std::vector socketSlots; - std::vector hosts; - std::vector hostSlots; + PollGroup kernel{&KernelSockets, {}, {}}; + PollGroup net{nullptr, {}, {}}; std::vector devices; std::vector requested; for (int descriptor = 0; descriptor < nfds; ++descriptor) { @@ -143,57 +148,50 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if (DescriptorSet(writefds, descriptor)) events |= KernelSocketPoll::Writable; if (DescriptorSet(exceptfds, descriptor)) events |= KernelSocketPoll::Urgent; if (events == 0) continue; - if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); + if (GuestSockets::IsOpen(descriptor)) { + kernel.entries.push_back({descriptor, events, 0}); + kernel.slots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); + } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); else { - sockets.push_back({descriptor, events, 0}); - socketSlots.push_back(requested.size()); + net.entries.push_back({descriptor, events, 0}); + net.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); } } for (const auto& device : devices) requested.push_back(device); const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); - const auto poller = g_socketPoller.load(); - if (!sockets.empty()) { - if (poller == nullptr) { - for (std::size_t index = 0; index < sockets.size();) { - const int descriptor = sockets[index].descriptor; + std::vector groups; + if (!kernel.entries.empty()) groups.push_back(std::move(kernel)); + if (!net.entries.empty()) { + PollGroup hosts{nullptr, {}, {}}; #ifndef _WIN32 - if (descriptor < GuestSockets::FirstDescriptor) { - hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); - hostSlots.push_back(socketSlots[index]); - sockets.erase(sockets.begin() + static_cast(index)); - socketSlots.erase(socketSlots.begin() + static_cast(index)); - continue; - } + hosts.poll = &HostPoll; #endif - UnknownDescriptor(descriptor, false); - } - } else { - for (auto& socket : sockets) socket.revents = 0; - const int probe = poller(sockets.data(), static_cast(sockets.size()), 0); + net.poll = g_socketPoller.load(); + if (net.poll != nullptr) { + for (auto& socket : net.entries) socket.revents = 0; + const int probe = net.poll(net.entries.data(), static_cast(net.entries.size()), 0); if (probe < 0) { *__error_nid_postfix() = -probe; return -1; } - for (std::size_t index = 0; index < sockets.size();) { - if ((sockets[index].revents & KernelSocketPoll::Unknown) == 0) { - ++index; - continue; - } - const int descriptor = sockets[index].descriptor; -#ifndef _WIN32 - if (descriptor < GuestSockets::FirstDescriptor) { - hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); - hostSlots.push_back(socketSlots[index]); - sockets.erase(sockets.begin() + static_cast(index)); - socketSlots.erase(socketSlots.begin() + static_cast(index)); - continue; - } -#endif - UnknownDescriptor(descriptor, true); - } } + for (std::size_t index = 0; index < net.entries.size();) { + if (net.poll != nullptr && (net.entries[index].revents & KernelSocketPoll::Unknown) == 0) { + ++index; + 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)); + net.slots.erase(net.slots.begin() + static_cast(index)); + } + if (!net.entries.empty()) groups.push_back(std::move(net)); + if (!hosts.entries.empty()) groups.push_back(std::move(hosts)); } const auto start = std::chrono::steady_clock::now(); const auto deadline = limit == nullptr ? std::chrono::steady_clock::time_point::max() @@ -206,48 +204,25 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; -#ifndef _WIN32 - if (!hosts.empty() && !sockets.empty()) { - if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); + if (groups.empty()) { + if (waitMilliseconds > 0) std::this_thread::sleep_for(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); for (;;) { - for (auto& host : hosts) host.revents = 0; - const int hostReady = HostPoll(hosts.data(), hosts.size(), SelectPollSliceMilliseconds); - if (hostReady < 0) { - *__error_nid_postfix() = errno; - return -1; + int groupReady = 0; + for (auto& group : groups) { + for (auto& entry : group.entries) entry.revents = 0; + const int result = group.poll(group.entries.data(), static_cast(group.entries.size()), slice); + if (result < 0) { + *__error_nid_postfix() = -result; + return -1; + } + for (std::size_t index = 0; index < group.entries.size(); ++index) requested[group.slots[index]].revents = group.entries[index].revents; + groupReady += result; } - for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; - for (auto& socket : sockets) socket.revents = 0; - const int socketReady = poller(sockets.data(), static_cast(sockets.size()), SelectPollSliceMilliseconds); - if (socketReady < 0) { - *__error_nid_postfix() = -socketReady; - return -1; - } - for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; - if (hostReady > 0 || socketReady > 0) break; - if (std::chrono::steady_clock::now() >= sliceEnd) break; + if (groups.size() == 1 || groupReady > 0 || std::chrono::steady_clock::now() >= sliceEnd) break; } - } else if (!hosts.empty()) { - for (auto& host : hosts) host.revents = 0; - if (HostPoll(hosts.data(), hosts.size(), waitMilliseconds) < 0) { - *__error_nid_postfix() = errno; - return -1; - } - for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; - } else -#endif - if (!sockets.empty()) { - if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); - for (auto& socket : sockets) socket.revents = 0; - const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); - if (result < 0) { - *__error_nid_postfix() = -result; - return -1; - } - for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; - } else if (waitMilliseconds > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); } int ready = 0; for (const auto& entry : requested) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 603a96cf35..7a0304ce10 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -2,9 +2,12 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include +#include #else #include #include +#include #include #include #include @@ -19,6 +22,7 @@ #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" +#include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include @@ -126,7 +130,56 @@ Waker& ThreadWaker() { } std::mutex socketsMutex; std::map> sockets; -int nextDescriptor = GuestSockets::FirstDescriptor; +#ifdef _WIN32 +int ReserveNumber(NativeSocket) { + const HANDLE placeholder = ::CreateFileW(L"NUL", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (placeholder == INVALID_HANDLE_VALUE) + throw std::runtime_error("socket: opening the NUL descriptor placeholder failed, error " + std::to_string(::GetLastError())); + const int number = ::_open_osfhandle(reinterpret_cast(placeholder), _O_RDONLY); + if (number < 0) { + ::CloseHandle(placeholder); + return Fail(24); + } + return number; +} +int CopyNumber(int number) { + const int copy = ::_dup(number); + return copy < 0 ? Fail(errno == EMFILE ? 24 : 9) : copy; +} +int CopyNumberTo(int number, int target) { + return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; +} +void ReleaseNumber(int number) { + ::_close(number); +} +#else +int ReserveNumber(NativeSocket native) { + const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); + return number < 0 ? Fail(NativeError()) : number; +} +int CopyNumber(int number) { + const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, 0); + return copy < 0 ? Fail(NativeError()) : copy; +} +int CopyNumberTo(int number, int target) { + return ::dup2(number, target) < 0 ? Fail(NativeError()) : 0; +} +void ReleaseNumber(int number) { + ::close(number); +} +#endif +int Install(std::shared_ptr socket) { + const int number = ReserveNumber(socket->value); + if (number < 0) return -1; + try { + std::lock_guard lock(socketsMutex); + sockets.insert_or_assign(number, std::move(socket)); + } catch (const std::bad_alloc&) { + ReleaseNumber(number); + return Fail(12); + } + return number; +} std::shared_ptr Lookup(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); @@ -167,6 +220,35 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, Fail(47); return false; } +std::uint16_t& PortOf(sockaddr_storage& address) { + return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; +} +int Disconnect(const Socket& socket) { + sockaddr_storage peer{}; + socklen_t peerSize = sizeof(peer); + if (::getpeername(socket.value, reinterpret_cast(&peer), &peerSize) != 0) return 0; + sockaddr_storage local{}; + socklen_t localSize = sizeof(local); + if (::getsockname(socket.value, reinterpret_cast(&local), &localSize) != 0) return Fail(NativeError()); +#ifdef _WIN32 + sockaddr_storage unspecified{}; + unspecified.ss_family = local.ss_family; + if (::connect(socket.value, reinterpret_cast(&unspecified), localSize) != 0) return Fail(NativeError()); +#else + sockaddr unspecified{}; + unspecified.sa_family = AF_UNSPEC; + if (::connect(socket.value, &unspecified, sizeof(unspecified)) != 0) return Fail(NativeError()); +#endif + sockaddr_storage current{}; + socklen_t currentSize = sizeof(current); + if (::getsockname(socket.value, reinterpret_cast(¤t), ¤tSize) != 0) return Fail(NativeError()); + if (PortOf(current) != 0) return 0; + sockaddr_storage wildcard{}; + wildcard.ss_family = local.ss_family; + PortOf(wildcard) = PortOf(local); + if (::bind(socket.value, reinterpret_cast(&wildcard), localSize) != 0) return Fail(NativeError()); + return 0; +} void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* length) { unsigned char bytes[28]{}; bytes[0] = native.ss_family == AF_INET ? 16 : 28; @@ -189,14 +271,31 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l } int GuestSockets::Close(int descriptor) { + std::shared_ptr released; { std::lock_guard lock(socketsMutex); - if (!sockets.erase(descriptor)) return Fail(9); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return Fail(9); + released = std::move(found->second); + sockets.erase(found); } EqueueDescriptorClosed(descriptor); + ReleaseNumber(descriptor); return 0; } +void GuestSockets::Forget(int descriptor) { + std::shared_ptr released; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return; + released = std::move(found->second); + sockets.erase(found); + } + EqueueDescriptorClosed(descriptor); +} + std::uintptr_t GuestSockets::CurrentWaker() { return static_cast(ThreadWaker().value); } @@ -267,6 +366,15 @@ extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { return GuestSockets::IsOpen(descriptor); } +extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native) { + const int number = ReserveNumber(static_cast(native)); + return number < 0 ? -*__error_nid_postfix() : number; +} + +extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number) { + ReleaseNumber(number); +} + namespace { #ifdef _WIN32 using NativePollDescriptor = WSAPOLLFD; @@ -285,44 +393,122 @@ struct PollFlag { constexpr short GuestPollPriority = 0x2; constexpr short GuestPollInvalid = 0x20; constexpr short GuestPollAccepted = 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100; +#ifdef _WIN32 +constexpr short NativePollIn = POLLRDNORM; +constexpr short NativePollPriority = POLLRDBAND; +#else +constexpr short NativePollIn = POLLIN; +constexpr short NativePollPriority = POLLPRI; +#endif constexpr PollFlag RequestFlags[] = { - {0x1, POLLIN}, {0x2, POLLPRI}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; + {0x1, NativePollIn}, {GuestPollPriority, NativePollPriority}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; constexpr PollFlag StatusFlags[] = {{0x8, POLLERR}, {0x10, POLLHUP}, {GuestPollInvalid, POLLNVAL}}; std::shared_ptr Find(int descriptor) { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); return found != sockets.end() ? found->second : nullptr; } +constexpr short GuestPollReadable = 0x1 | 0x40; +constexpr short GuestPollWritable = 0x4; +constexpr short GuestPollHangUp = 0x10; +constexpr short GuestPollError = 0x8; +[[noreturn]] void ThrowReadEndWritable(int descriptor) { + throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported"); +} +#ifdef _WIN32 +struct PipeIoStatus { + void* status; + ULONG_PTR information; +}; +struct PipeLocalInformation { + ULONG type; + ULONG configuration; + ULONG maximumInstances; + ULONG currentInstances; + ULONG inboundQuota; + ULONG readDataAvailable; + ULONG outboundQuota; + ULONG writeQuotaAvailable; + ULONG state; + ULONG end; +}; +using QueryInformationFile = LONG(NTAPI*)(HANDLE, PipeIoStatus*, void*, ULONG, int); +constexpr int FileAccessInformationClass = 8; +constexpr int FilePipeLocalInformationClass = 24; +constexpr ULONG PipeClosingState = 4; +constexpr ULONG PipeAtomicBytes = 512; +bool PipeReadiness(int descriptor, short events, short& revents) { + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE || GetFileType(handle) != FILE_TYPE_PIPE) return false; + static const auto query = reinterpret_cast( + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile"))); + if (!query) throw std::runtime_error("poll: NtQueryInformationFile is unavailable"); + PipeIoStatus status{}; + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || + query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) + throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + if ((events & GuestPollWritable) && !(access & FILE_WRITE_DATA)) ThrowReadEndWritable(descriptor); + const bool closed = pipe.state == PipeClosingState; + revents = 0; + if ((access & FILE_READ_DATA) && pipe.readDataAvailable > 0) + revents = static_cast(revents | (events & GuestPollReadable)); + if ((access & FILE_WRITE_DATA) && !closed && pipe.writeQuotaAvailable >= PipeAtomicBytes) + revents = static_cast(revents | (events & GuestPollWritable)); + if (closed) revents = static_cast(revents | (events & GuestPollReadable) | GuestPollHangUp); + return true; +} +#else +bool IsPipe(int descriptor, short events) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0 || !S_ISFIFO(status.st_mode)) return false; + const int flags = ::fcntl(descriptor, F_GETFL); + if (flags < 0) throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + if ((events & GuestPollWritable) && (flags & O_ACCMODE) == O_RDONLY) ThrowReadEndWritable(descriptor); + return true; +} +#endif } int GuestSockets::Duplicate(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return Fail(9); + const int duplicate = CopyNumber(descriptor); + if (duplicate < 0) return -1; try { - std::lock_guard lock(socketsMutex); - const auto found = sockets.find(descriptor); - if (found == sockets.end()) return Fail(9); - if (nextDescriptor == INT_MAX) return Fail(24); - const int duplicate = nextDescriptor; - sockets.emplace(duplicate, found->second); - ++nextDescriptor; - return duplicate; + sockets.insert_or_assign(duplicate, found->second); } catch (const std::bad_alloc&) { + ReleaseNumber(duplicate); return Fail(12); } + return duplicate; } int GuestSockets::DuplicateTo(int descriptor, int target) { - try { + std::shared_ptr replaced; + { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return Fail(9); if (target == descriptor) return target; - if (target == INT_MAX) return Fail(9); - sockets.insert_or_assign(target, found->second); - if (target >= nextDescriptor) nextDescriptor = target + 1; - return target; - } catch (const std::bad_alloc&) { - return Fail(12); + if (CopyNumberTo(descriptor, target) != 0) return -1; + const auto previous = sockets.find(target); + if (previous != sockets.end()) { + replaced = std::move(previous->second); + previous->second = found->second; + } else { + try { + sockets.emplace(target, found->second); + } catch (const std::bad_alloc&) { + ReleaseNumber(target); + return Fail(12); + } + } } + if (replaced) EqueueDescriptorClosed(target); + return target; } namespace { @@ -444,9 +630,54 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e return true; } +int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { + std::vector> held; + std::vector native; +#ifdef _WIN32 + constexpr short NativeReadable = POLLRDNORM; + constexpr short NativeUrgent = POLLRDBAND; + constexpr short NativeWritable = POLLWRNORM; +#else + constexpr short NativeReadable = POLLIN; + constexpr short NativeUrgent = POLLPRI; + constexpr short NativeWritable = POLLOUT; +#endif + for (int index = 0; index < count; ++index) { + auto socket = Find(entries[index].descriptor); + if (!socket) return -9; + NativePollDescriptor request{}; + request.fd = socket->value; + if (entries[index].events & KernelSocketPoll::Readable) request.events = static_cast(request.events | NativeReadable); + if (entries[index].events & KernelSocketPoll::Urgent) request.events = static_cast(request.events | NativeUrgent); + if (entries[index].events & KernelSocketPoll::Writable) request.events = static_cast(request.events | NativeWritable); + native.push_back(request); + held.push_back(std::move(socket)); + } + if (native.empty()) return 0; +#ifdef _WIN32 + const int result = WSAPoll(native.data(), static_cast(native.size()), timeoutMilliseconds); +#else + const int result = ::poll(native.data(), static_cast(native.size()), timeoutMilliseconds); +#endif + if (result < 0) return -NativeError(); + int ready = 0; + for (int index = 0; index < count; ++index) { + const short events = native[static_cast(index)].revents; + short revents = 0; + if (events & NativeReadable) revents = static_cast(revents | KernelSocketPoll::Readable); + if (events & NativeUrgent) revents = static_cast(revents | KernelSocketPoll::Urgent); + if (events & NativeWritable) revents = static_cast(revents | KernelSocketPoll::Writable); + if (events & (POLLERR | POLLNVAL)) revents = static_cast(revents | KernelSocketPoll::Error); + if (events & POLLHUP) revents = static_cast(revents | KernelSocketPoll::HangUp); + entries[index].revents = revents; + if (revents != 0) ++ready; + } + return ready; +} + extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { - if (descriptor < GuestSockets::FirstDescriptor) { + if (!GuestSockets::IsOpen(descriptor)) { const auto type = File::FcntlArgumentOf(command); std::intptr_t argument = 0; if (type != File::FcntlArgument::None) { @@ -496,14 +727,14 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { int netError = 0; - if (descriptor < GuestSockets::FirstDescriptor && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { + if (!GuestSockets::IsOpen(descriptor) && KernelSetSocketOption_nid_no_patch(descriptor, level, option, value, length, &netError)) { return netError == 0 ? 0 : Fail(netError); } 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); + if (!value || length < sizeof(int)) return Fail(22); return ::setsockopt(socket->value, SOL_SOCKET, nativeOption, static_cast(value), sizeof(int)) ? Fail(NativeError()) : 0; } @@ -543,11 +774,7 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { auto socket = std::make_shared(native, family, plainType); guard.value = Invalid; if (nonblocking && SetNonBlocking(*socket)) return -1; - std::lock_guard lock(socketsMutex); - if (nextDescriptor == INT_MAX) return Fail(24); - const int descriptor = nextDescriptor++; - sockets.emplace(descriptor, std::move(socket)); - return descriptor; + return Install(std::move(socket)); } catch (const std::bad_alloc&) { return Fail(12); } @@ -576,16 +803,13 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de auto second = std::make_shared(native[1], GuestUnix, plainType); secondGuard.value = Invalid; if (nonblocking && (SetNonBlocking(*first) || SetNonBlocking(*second))) return -1; - std::lock_guard lock(socketsMutex); - if (nextDescriptor >= INT_MAX - 1) return Fail(24); - const int firstDescriptor = nextDescriptor++; - const int secondDescriptor = nextDescriptor++; - sockets.emplace(firstDescriptor, std::move(first)); - try { - sockets.emplace(secondDescriptor, std::move(second)); - } catch (const std::bad_alloc&) { - sockets.erase(firstDescriptor); - return Fail(12); + const int firstDescriptor = Install(std::move(first)); + if (firstDescriptor < 0) return -1; + const int secondDescriptor = Install(std::move(second)); + if (secondDescriptor < 0) { + const int error = *__error_nid_postfix(); + GuestSockets::Close(firstDescriptor); + return Fail(error); } descriptors[0] = firstDescriptor; descriptors[1] = secondDescriptor; @@ -597,6 +821,9 @@ int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* de int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) { const auto socket = Lookup(descriptor); if (!socket) return -1; + if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 + && Disconnect(*socket) != 0) + return -1; sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; @@ -622,10 +849,14 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l try { auto accepted = std::make_shared(native, listener->family, listener->type); guard.value = Invalid; - std::lock_guard lock(socketsMutex); - if (nextDescriptor == INT_MAX) return Fail(24); - const int acceptedDescriptor = nextDescriptor++; - sockets.emplace(acceptedDescriptor, std::move(accepted)); + bool inheritNonblocking; + { + std::lock_guard modeLock(listener->modeMutex); + inheritNonblocking = listener->nonblocking; + } + if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; + const int acceptedDescriptor = Install(std::move(accepted)); + if (acceptedDescriptor < 0) return -1; if (address) GuestAddress(peer, address, length); return acceptedDescriptor; } catch (const std::bad_alloc&) { @@ -677,7 +908,15 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32 if (socket->family == GuestUnix) return UnnamedAddress(address, length); sockaddr_storage native{}; socklen_t size = sizeof(native); - if (::getsockname(socket->value, reinterpret_cast(&native), &size)) return Fail(NativeError()); + if (::getsockname(socket->value, reinterpret_cast(&native), &size)) { +#ifdef _WIN32 + if (WSAGetLastError() != WSAEINVAL) return Fail(NativeError()); + native = {}; + native.ss_family = socket->family == 2 ? AF_INET : AF_INET6; +#else + return Fail(NativeError()); +#endif + } GuestAddress(native, address, length); return 0; } @@ -693,7 +932,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 return 0; } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { - if (descriptor < GuestSockets::FirstDescriptor) return File::Ioctl(descriptor, request, argument); + if (!GuestSockets::IsOpen(descriptor)) return File::Ioctl(descriptor, request, argument); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); @@ -942,6 +1181,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c std::vector> held; std::vector native; std::vector owners; + std::vector pipes; int ready = 0; for (std::uint32_t i = 0; i < count; ++i) { auto& entry = descriptors[i]; @@ -949,17 +1189,23 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.descriptor < 0) continue; if ((entry.events & ~GuestPollAccepted) != 0) throw std::runtime_error("poll: unsupported event flags " + std::to_string(entry.events)); - if (entry.descriptor < GuestSockets::FirstDescriptor) - throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is not a socket"); auto socket = Find(entry.descriptor); if (!socket) { - entry.revents = GuestPollInvalid; - ++ready; + if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) { + entry.revents = GuestPollInvalid; + ++ready; + continue; + } +#ifdef _WIN32 + short revents = 0; + if (!PipeReadiness(entry.descriptor, entry.events, revents)) +#else + if (!IsPipe(entry.descriptor, entry.events)) +#endif + throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket nor a pipe"); + pipes.push_back(i); continue; } -#ifdef _WIN32 - if (entry.events & GuestPollPriority) throw std::runtime_error("poll: POLLPRI is not supported by WSAPoll"); -#endif NativePollDescriptor request{}; request.fd = socket->value; for (const auto& flag : RequestFlags) @@ -968,29 +1214,76 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c owners.push_back(i); held.push_back(std::move(socket)); } - if (native.empty()) { +#ifndef _WIN32 + const std::size_t socketCount = native.size(); + for (const auto index : pipes) { + NativePollDescriptor request{}; + request.fd = descriptors[index].descriptor; + for (const auto& flag : RequestFlags) + if (descriptors[index].events & flag.guest) request.events = static_cast(request.events | flag.native); + native.push_back(request); + owners.push_back(index); + } +#endif + if (native.empty() && pipes.empty()) { if (ready || timeout == 0) return ready; - if (timeout < 0) throw std::runtime_error("poll: an infinite wait without sockets never returns"); + 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)); return 0; } - const int wait = ready ? 0 : timeout; -#ifdef _WIN32 - const int result = WSAPoll(native.data(), static_cast(native.size()), wait); -#else - const int result = ::poll(native.data(), static_cast(native.size()), wait); -#endif - if (result < 0) return Fail(NativeError()); - for (std::size_t i = 0; i < native.size(); ++i) { + const auto collect = [&](std::size_t i, bool pipe) { auto& entry = descriptors[owners[i]]; for (const auto& flag : RequestFlags) if ((entry.events & flag.guest) && (native[i].revents & flag.native)) entry.revents = static_cast(entry.revents | flag.guest); for (const auto& flag : StatusFlags) if (native[i].revents & flag.native) entry.revents = static_cast(entry.revents | flag.guest); + if (pipe && (entry.revents & GuestPollError)) entry.revents = static_cast((entry.revents & ~GuestPollError) | GuestPollHangUp); + if (pipe && (entry.revents & GuestPollHangUp)) + entry.revents = static_cast((entry.revents & ~GuestPollWritable) | (entry.events & GuestPollReadable)); if (entry.revents) ++ready; + }; +#ifdef _WIN32 + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout < 0 ? 0 : timeout); + for (;;) { + int pipeReady = 0; + for (const auto index : pipes) { + auto& entry = descriptors[index]; + PipeReadiness(entry.descriptor, entry.events, entry.revents); + if (entry.revents) ++pipeReady; + } + int slice = 0; + if (!ready && !pipeReady) { + if (pipes.empty()) slice = timeout; + else if (timeout < 0) slice = 1; + else { + const auto left = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); + slice = static_cast(std::clamp(left, 0, 1)); + } + } + if (!native.empty()) { + for (auto& request : native) request.revents = 0; + if (WSAPoll(native.data(), static_cast(native.size()), slice) < 0) return Fail(NativeError()); + } else if (slice > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(slice)); + } + int socketReady = 0; + for (const auto& request : native) + if (request.revents) ++socketReady; + const bool expired = timeout >= 0 && std::chrono::steady_clock::now() >= deadline; + if (ready || pipeReady || socketReady || expired || pipes.empty()) { + ready += pipeReady; + for (std::size_t i = 0; i < native.size(); ++i) collect(i, false); + return ready; + } } +#else + const int wait = ready ? 0 : timeout; + const int result = ::poll(native.data(), static_cast(native.size()), wait); + if (result < 0) return Fail(NativeError()); + for (std::size_t i = 0; i < native.size(); ++i) collect(i, i >= socketCount); return ready; +#endif } } diff --git a/core/libs/prx/libkernel/System/include/ThermalSensors.hpp b/core/libs/prx/libkernel/System/include/ThermalSensors.hpp new file mode 100644 index 0000000000..7f8c299d01 --- /dev/null +++ b/core/libs/prx/libkernel/System/include/ThermalSensors.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_SYSTEM_THERMALSENSORS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_SYSTEM_THERMALSENSORS_HPP + +extern "C" void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]); + +#endif diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 3e271d15b3..1a42cca261 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -9,6 +11,22 @@ static std::atomic g_coredumpHandler{0}; static std::atomic g_coredumpContext{0}; +static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast(0x81180003u); + +static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { + if (mem == nullptr || size == 0 || size > UINTPTR_MAX - reinterpret_cast(mem)) + throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); +} + +static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { + requireRegionOutsideHandler(function, mem, size); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +static void requireTextOutsideHandler(const char* function, const char* argument, const char* text) { + if (text == nullptr) throw std::runtime_error(std::string(function) + ": null " + argument + " outside the core dump handler"); +} + extern "C" { int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stack_size, uint64_t context) { @@ -31,9 +49,10 @@ int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_ } -int APS5_VABI sceCoredumpAttachUserFile(void) { - NotImplemented_nid_no_patch("5nc2gdLNsok"); - return 0; +int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { + (void)userValue; + requireTextOutsideHandler(__func__, "path", path); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { @@ -41,9 +60,11 @@ int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { return 0; } -int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(void) { - NotImplemented_nid_no_patch("MEJ7tc7ThwM"); - return 0; +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { + (void)userValue; + requireRegionOutsideHandler(__func__, mem, size); + requireTextOutsideHandler(__func__, "name", name); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpSetUserDataType(void) { @@ -51,14 +72,24 @@ int APS5_VABI sceCoredumpSetUserDataType(void) { return 0; } -int APS5_VABI sceCoredumpDebugTextOut(void) { - NotImplemented_nid_no_patch("dei8oUx6DbU"); - return 0; +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len) { + if (len < 0 || (str == nullptr && len != 0)) + throw std::runtime_error(std::string(__func__) + ": null text or negative length"); + if (len == 0) return; + const int shown = str[len - 1] == '\n' ? len - 1 : len; + std::fprintf(stderr, "[coredump] %.*s\n", shown, str); } -int APS5_VABI sceCoredumpGetStopInfoCpu(void) { - NotImplemented_nid_no_patch("kK0DUW1Ukgc"); - return 0; +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size) { + requireRegionOutsideHandler(__func__, data, size); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; +} + +int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { + const auto address = reinterpret_cast(info); + if (info == nullptr || size != 16 || address % alignof(uint64_t) != 0 || size > UINTPTR_MAX - address) + throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported CPU stop-info buffer outside the core dump handler"); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { @@ -73,13 +104,13 @@ int APS5_VABI sceCoredumpWriteUserString() { return 0; } -int APS5_VABI sceCoredumpAttachUserMemoryFile(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { + (void)userValue; + return attachOutsideCoredumpHandler(__func__, mem, size); } -int APS5_VABI sceCoredumpAttachMemoryRegion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size) { + (void)userValue; + return attachOutsideCoredumpHandler(__func__, mem, size); } } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index f0f00b664a..2f498dca06 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -40,10 +40,12 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); +constexpr int errnoNoSuchProcess = 3; constexpr int errnoNoChild = 10; constexpr int errnoBadAddress = 14; constexpr int errnoInvalidArgument = 22; @@ -116,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; @@ -214,8 +221,9 @@ int APS5_VABI getpid_nid_postfix(void) { } int APS5_VABI getsid_nid_postfix(int pid) { - (void)pid; - NotImplemented_nid_no_patch("getsid: process sessions"); + const int self = getpid_nid_postfix(); + if (pid == 0 || pid == self) return self; + *__error_nid_postfix() = errnoNoSuchProcess; return -1; } @@ -300,6 +308,33 @@ int APS5_VABI waitpid_nid_postfix(int pid, int* status, int options) { return -1; } +int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit); + +int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limit) { + if (limit == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } + if (resource < 0 || resource >= guestRlimitCount) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + + GuestResourceLimit requested = *limit; + if (requested.rlim_cur < 0) + requested.rlim_cur = guestRlimitInfinity; + if (requested.rlim_max < 0) + requested.rlim_max = guestRlimitInfinity; + if (requested.rlim_cur > requested.rlim_max) + requested.rlim_cur = requested.rlim_max; + + GuestResourceLimit current{}; + getrlimit_nid_postfix(resource, ¤t); + if (requested.rlim_cur != current.rlim_cur || requested.rlim_max != current.rlim_max) + throw std::runtime_error("setrlimit: changing the limit of resource " + std::to_string(resource) + " is not supported"); + return 0; +} + int APS5_VABI wait_nid_postfix(int* status) { return waitpid_nid_postfix(-1, status, 0); } @@ -318,6 +353,14 @@ int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { return -1; } +int APS5_VABI execve_nid_postfix(const char* path, char* const* arguments, char* const* environment) { + (void)path; + (void)arguments; + (void)environment; + NotImplemented_nid_no_patch("execve: executable replacement"); + return -1; +} + int APS5_VABI sceKernelGetCurrentCpu(void) { #ifdef _WIN32 PROCESSOR_NUMBER processor{}; @@ -510,6 +553,20 @@ 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 sceKernelIsTrinityMode(void) { return 0; } @@ -548,6 +605,15 @@ int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { return 0; } +int APS5_VABI setregid_nid_postfix(std::uint32_t rgid, std::uint32_t egid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (rgid != 0 && rgid != unchanged) + throw std::runtime_error("setregid: unsupported real group id " + std::to_string(rgid)); + if (egid != 0 && egid != unchanged) + throw std::runtime_error("setregid: unsupported effective group id " + std::to_string(egid)); + return 0; +} + int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { if (count < 0 || count > freebsdGroupsMax) { *__error_nid_postfix() = errnoInvalidArgument; diff --git a/core/libs/prx/libkernel/System/src/Sanitizer.cpp b/core/libs/prx/libkernel/System/src/Sanitizer.cpp index 15f2d3f5fb..0586eef7d7 100644 --- a/core/libs/prx/libkernel/System/src/Sanitizer.cpp +++ b/core/libs/prx/libkernel/System/src/Sanitizer.cpp @@ -6,6 +6,10 @@ int APS5_VABI sceKernelIsAddressSanitizerEnabled(void) { return 0; } +int APS5_VABI sceKernelIsThreadSanitizerEnabled(void) { + return 0; +} + MallocReplace* APS5_VABI sceKernelGetSanitizerMallocReplaceExternal(void) { static MallocReplace replacement{}; return &replacement; diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 0e06f15383..4629df138d 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -44,6 +45,88 @@ std::atomic blockedMask{0}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; +constexpr int GuestSigblock = 1; +constexpr int GuestSigunblock = 2; +constexpr int GuestSigsetmask = 3; +constexpr int GuestEintr = 4; +constexpr int GuestEagain = 35; +constexpr int GuestSiuser = 0; +struct GuestSiginfo { + std::int32_t signo; + std::int32_t error; + std::int32_t code; + std::int32_t pid; + std::uint32_t uid; + std::int32_t status; + std::uintptr_t address; + std::int32_t value; + std::int32_t valueTail; + std::int64_t band; + std::int32_t spare[7]; + std::int32_t tail; +}; +static_assert(sizeof(GuestSiginfo) == 80 && offsetof(GuestSiginfo, code) == 8 && offsetof(GuestSiginfo, pid) == 12 && + offsetof(GuestSiginfo, uid) == 16 && offsetof(GuestSiginfo, address) == 24 && offsetof(GuestSiginfo, value) == 32 && + offsetof(GuestSiginfo, band) == 40); +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; +static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } +std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } +bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } +bool SetHasSignal(const GuestSignalSet& set) { return (set.bits[0] | set.bits[1] | set.bits[2] | set.bits[3]) != 0; } +bool SignalBlocked(int guest) { return guest <= 32 && (blockedMask.load() & SignalBit(guest)) != 0; } + +struct PendingSignals { + std::mutex lock; + std::condition_variable changed; + std::atomic words[4]{}; +}; + +PendingSignals& Pending() { + static auto* state = new PendingSignals(); + return *state; +} + +void MarkPending(int guest) { + auto& pending = Pending(); + { + // under the same lock the waits check the set with: without it a signal raised + // between the check and the wait is lost and the wait never returns + std::lock_guard lock(pending.lock); + pending.words[SignalWord(guest)].fetch_or(SignalBit(guest), std::memory_order_relaxed); + } + pending.changed.notify_all(); +} + +bool TakePending(int guest) { + auto& pending = Pending(); + auto& word = pending.words[SignalWord(guest)]; + const std::uint32_t bit = SignalBit(guest); + if ((word.load(std::memory_order_relaxed) & bit) == 0) return false; + word.fetch_and(~bit, std::memory_order_relaxed); + return true; +} + +bool PendingInSet(const GuestSignalSet& set, int* guest) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (!SignalInSet(set, candidate) || !TakePending(candidate)) continue; + *guest = candidate; + return true; + } + return false; +} + +bool PendingInSetPresent(const GuestSignalSet& set) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (!SignalInSet(set, candidate)) continue; + if ((Pending().words[SignalWord(candidate)].load(std::memory_order_relaxed) & SignalBit(candidate)) != 0) return true; + } + return false; +} + int NativeSignal(int guest) { switch (guest) { case 2: return SIGINT; @@ -64,7 +147,10 @@ void Dispatch(int native) { // Preserve the guest's persistent registration across CRT delivery. std::signal(native, Dispatch); #endif - if ((blockedMask.load() & (1u << (guest - 1))) != 0) return; + if (SignalBlocked(guest)) { + MarkPending(guest); + return; + } const auto callback = handlers[guest].load(); if (reinterpret_cast(callback) > 1) callback(guest); } @@ -84,12 +170,79 @@ bool Install(int guest, const GuestSigaction& action) { } } +namespace { + +GuestSigaction Disposition(int guest) { + std::lock_guard lock(registration); + return dispositions[guest]; +} + +bool CaughtByGuest(int guest) { + return Disposition(guest).handler > 1; +} + +bool Deliverable(int guest) { + return NativeSignal(guest) != 0 || CaughtByGuest(guest); +} + +bool TakePendingUnblocked(int* guest) { + for (int candidate = 1; candidate <= MaxSignal; ++candidate) { + if (SignalBlocked(candidate) || !Deliverable(candidate)) continue; + if (!TakePending(candidate)) continue; + *guest = candidate; + return true; + } + return false; +} + +bool DeliverPendingSignal(int guest) { + const GuestSigaction action = Disposition(guest); + if (action.handler == 1) return false; + if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("signal: SA_SIGINFO handlers are not delivered"); + const int native = NativeSignal(guest); + if (native != 0) { + if (std::raise(native) != 0) throw std::runtime_error("signal: delivering the signal to the host failed"); + return action.handler > 1; + } + if (action.handler > 1) { + reinterpret_cast(action.handler)(guest); + return true; + } + return false; +} + +int WaitForPendingInSet(const GuestSignalSet& set, bool waitForever, const GuestTimespec* timeout, int* guest) { + auto& pending = Pending(); + std::unique_lock lock(pending.lock); + for (;;) { + if (PendingInSet(set, guest)) return 0; + if (waitForever) { + pending.changed.wait(lock); + continue; + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + + std::chrono::nanoseconds(timeout->nanoseconds); + if (pending.changed.wait_until(lock, deadline) == std::cv_status::no_timeout) continue; + return PendingInSet(set, guest) ? 0 : GuestEagain; + } +} + +void FillSiginfo(GuestSiginfo* info, int guest) { + *info = {}; + info->signo = guest; + info->code = GuestSiuser; + info->pid = getpid_nid_postfix(); +} + +} + namespace { constexpr int GuestSigalrm = 14; struct AlarmState { std::mutex lock; std::condition_variable changed; std::optional deadline; + std::chrono::microseconds interval{0}; bool threadStarted = false; }; @@ -98,19 +251,28 @@ AlarmState& Alarms() { return *state; } -void ExpireAlarm() { +std::string DescribeInterval(std::chrono::microseconds interval) { + char text[64]; + std::snprintf(text, sizeof(text), "%lld.%06lld s", static_cast(interval.count() / 1000000), + static_cast(interval.count() % 1000000)); + return text; +} + +void ExpireAlarm(std::chrono::microseconds interval) { std::uintptr_t handler = 0; { std::lock_guard lock(registration); handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; + if ((blockedMask.load() & (1u << (GuestSigalrm - 1))) != 0) + throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired while SIGALRM is blocked: pending signals are not implemented"); if (handler == 0) { std::fprintf(stderr, "SIGALRM: the alarm expired with the default disposition; the process terminates\n"); std::fflush(stderr); std::_Exit(128 + GuestSigalrm); } - throw std::runtime_error("alarm: delivering SIGALRM to a guest handler is not implemented"); + throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired: delivering SIGALRM to a guest handler is not implemented"); } void RunAlarms() { @@ -123,30 +285,43 @@ void RunAlarms() { } const auto deadline = *alarms.deadline; if (alarms.changed.wait_until(lock, deadline) != std::cv_status::timeout || alarms.deadline != deadline) continue; - alarms.deadline.reset(); + const auto interval = alarms.interval; + if (interval.count() == 0) alarms.deadline.reset(); + else { + const auto now = std::chrono::steady_clock::now(); + const auto periods = now < deadline ? 1 : (now - deadline) / interval + 1; + alarms.deadline = deadline + periods * interval; + } lock.unlock(); - ExpireAlarm(); + ExpireAlarm(interval); lock.lock(); } } + +std::chrono::microseconds RemainingAlarm(const AlarmState& alarms, std::chrono::steady_clock::time_point now) { + if (!alarms.deadline || *alarms.deadline <= now) return std::chrono::microseconds(0); + return std::chrono::duration_cast(*alarms.deadline - now); +} + +void NotifyAlarms(AlarmState& alarms) { + if (!alarms.threadStarted) { + std::thread(RunAlarms).detach(); + alarms.threadStarted = true; + } + alarms.changed.notify_all(); +} } extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) { auto& alarms = Alarms(); std::lock_guard lock(alarms.lock); const auto now = std::chrono::steady_clock::now(); - unsigned int remaining = 0; - if (alarms.deadline && *alarms.deadline > now) { - const auto micros = std::chrono::duration_cast(*alarms.deadline - now).count(); - remaining = static_cast((micros + 999999) / 1000000); - } + const auto micros = RemainingAlarm(alarms, now).count(); + const auto remaining = static_cast((micros + 999999) / 1000000); + alarms.interval = std::chrono::microseconds(0); if (seconds == 0) alarms.deadline.reset(); else alarms.deadline = now + std::chrono::seconds(seconds); - if (!alarms.threadStarted) { - std::thread(RunAlarms).detach(); - alarms.threadStarted = true; - } - alarms.changed.notify_all(); + NotifyAlarms(alarms); return remaining; } @@ -176,11 +351,73 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output result.value.tv_sec = micros / 1000000; result.value.tv_usec = micros % 1000000; } + const auto interval = alarms.interval.count(); + result.interval.tv_sec = interval / 1000000; + result.interval.tv_usec = interval % 1000000; } *output = result; return 0; } +namespace { +constexpr int ItimerReal = 0; +constexpr int ItimerProf = 2; +constexpr std::int64_t MaxItimerSeconds = 0x7fffffff / 2; +constexpr std::int64_t MinExactIntervalMicros = 1000; + +bool ValidItimer(const KernelTimeval& time) { + return time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000 && time.tv_sec <= MaxItimerSeconds; +} + +bool ItimerSet(const KernelTimeval& time) { + return time.tv_sec != 0 || time.tv_usec != 0; +} + +std::chrono::microseconds ItimerDuration(const KernelTimeval& time) { + return std::chrono::seconds(time.tv_sec) + std::chrono::microseconds(time.tv_usec); +} + +KernelTimeval ItimerTimeval(std::chrono::microseconds duration) { + return {duration.count() / 1000000, duration.count() % 1000000}; +} +} + +extern "C" int APS5_VABI setitimer_nid_postfix(int which, const GuestItimerval* value, GuestItimerval* previous) { + if (which < ItimerReal || which > ItimerProf) { *__error_nid_postfix() = 22; return -1; } + if (value == nullptr && previous == nullptr) { *__error_nid_postfix() = 14; return -1; } + GuestItimerval requested{}; + if (value != nullptr) { + requested = *value; + if (!ValidItimer(requested.value)) { *__error_nid_postfix() = 22; return -1; } + if (!ItimerSet(requested.value)) requested.interval = {}; + else if (!ValidItimer(requested.interval)) { *__error_nid_postfix() = 22; return -1; } + } + if (which != ItimerReal) { + if (ItimerSet(requested.value)) + throw std::runtime_error(std::string("setitimer: ") + (which == ItimerProf ? "ITIMER_PROF" : "ITIMER_VIRTUAL") + " needs process CPU time accounting, which is not implemented"); + if (previous != nullptr) *previous = {}; + return 0; + } + if (requested.interval.tv_sec == 0 && requested.interval.tv_usec != 0 && requested.interval.tv_usec < MinExactIntervalMicros) + throw std::runtime_error("setitimer: it_interval " + DescribeInterval(ItimerDuration(requested.interval)) + " is below 1 ms, where the kernel rounds it up to a clock tick that is not known"); + GuestItimerval old{}; + { + auto& alarms = Alarms(); + std::lock_guard lock(alarms.lock); + const auto now = std::chrono::steady_clock::now(); + old.interval = ItimerTimeval(alarms.interval); + old.value = ItimerTimeval(RemainingAlarm(alarms, now)); + if (value != nullptr) { + alarms.interval = ItimerDuration(requested.interval); + if (ItimerSet(requested.value)) alarms.deadline = now + ItimerDuration(requested.value); + else alarms.deadline.reset(); + NotifyAlarms(alarms); + } + } + if (previous != nullptr) *previous = old; + return 0; +} + struct GuestStack { void* sp; std::size_t size; @@ -217,15 +454,22 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { - const int native = NativeSignal(guest); - if (!native) { *__error_nid_postfix() = 22; return -1; } - { - std::lock_guard lock(registration); - if (dispositions[guest].handler > 1 && !Delivered(dispositions[guest])) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } + const GuestSigaction action = Disposition(guest); + if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (SignalBlocked(guest)) { + if (action.handler != 1) MarkPending(guest); + return 0; } - const int result = std::raise(native); - if (result) *__error_nid_postfix() = 22; - return result ? -1 : 0; + const int native = NativeSignal(guest); + if (native != 0) { + const int result = std::raise(native); + if (result) *__error_nid_postfix() = 22; + return result ? -1 : 0; + } + if (!CaughtByGuest(guest)) { *__error_nid_postfix() = 22; return -1; } + DeliverPendingSignal(guest); + return 0; } int APS5_VABI kill_nid_postfix(int pid, int guest) { if (guest < 0 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } @@ -233,12 +477,18 @@ int APS5_VABI kill_nid_postfix(int pid, int guest) { if (pid != self && pid != 0 && pid != -self) { *__error_nid_postfix() = 3; return -1; } return guest == 0 ? 0 : raise_nid_postfix(guest); } +int APS5_VABI killpg_nid_postfix(int pgrp, int guest) { + if (pgrp < 0) { *__error_nid_postfix() = 22; return -1; } + return kill_nid_postfix(-pgrp, guest); +} int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest) { if (guest < 0 || guest > MaxSignal) return PosixThread::GUEST_EINVAL; if (thread == nullptr) return PosixThread::GUEST_ESRCH; if (guest == 0) return 0; - if (thread != scePthreadSelf()) NotImplemented_nid_no_patch(("pthread_kill signal " + std::to_string(guest) + " to another thread").c_str()); - return raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); + if (thread == scePthreadSelf()) return raise_nid_postfix(guest) == 0 ? 0 : *__error_nid_postfix(); + if (!GuestThreadIsLive(thread)) return PosixThread::GUEST_ESRCH; + if (Disposition(guest).handler != 1) MarkPending(guest); + return 0; } int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previous) { GuestStack replacement = alternateStack; @@ -253,20 +503,26 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { - std::lock_guard lock(registration); - if (previousSet != nullptr) { - previousSet->bits[0] = blockedMask.load(); - previousSet->bits[1] = 0; - previousSet->bits[2] = 0; - previousSet->bits[3] = 0; - } - if (set != nullptr) { - switch (how) { - case 1: blockedMask.fetch_or(set->bits[0]); break; - case 2: blockedMask.fetch_and(~set->bits[0]); break; - case 3: blockedMask.store(set->bits[0]); break; - default: throw std::invalid_argument("_sigprocmask: invalid how"); + { + std::lock_guard lock(registration); + if (previousSet != nullptr) { + previousSet->bits[0] = blockedMask.load(); + previousSet->bits[1] = 0; + previousSet->bits[2] = 0; + previousSet->bits[3] = 0; } + if (set != nullptr) { + switch (how) { + case 1: blockedMask.fetch_or(set->bits[0]); break; + case 2: blockedMask.fetch_and(~set->bits[0]); break; + case 3: blockedMask.store(set->bits[0]); break; + default: throw std::invalid_argument("_sigprocmask: invalid how"); + } + } + } + for (int guest = 1; guest <= MaxSignal; ++guest) { + if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; + DeliverPendingSignal(guest); } return 0; } @@ -281,31 +537,62 @@ int APS5_VABI pthread_sigmask_nid_postfix(int how, const GuestSignalSet* set, Gu return _sigprocmask_nid_postfix(how, set, previousSet); } -int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, const void* timeout) { - (void)set; - (void)info; - (void)timeout; - NotImplemented_nid_no_patch("sigtimedwait: waiting for a pending signal"); - return -1; +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { + if (set == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + if (timeout != nullptr && (timeout->nanoseconds < 0 || timeout->nanoseconds >= 1000000000) && !PendingInSetPresent(*set)) { + *__error_nid_postfix() = 22; + return -1; + } + int guest = 0; + const int result = WaitForPendingInSet(*set, timeout == nullptr, timeout, &guest); + if (result != 0) { *__error_nid_postfix() = result; return -1; } + if (info != nullptr) FillSiginfo(static_cast(info), guest); + return guest; } int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { - (void)set; - (void)info; - NotImplemented_nid_no_patch("sigwaitinfo: waiting for a pending signal"); - return -1; + if (set == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + int guest = 0; + if (WaitForPendingInSet(*set, true, nullptr, &guest) != 0) { *__error_nid_postfix() = GuestEagain; return -1; } + if (info != nullptr) FillSiginfo(static_cast(info), guest); + return guest; } int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { - (void)set; - (void)received; - NotImplemented_nid_no_patch("sigwait: waiting for a pending signal"); - return -1; + if (set == nullptr || received == nullptr || !SetHasSignal(*set)) { *__error_nid_postfix() = 22; return -1; } + int guest = 0; + if (WaitForPendingInSet(*set, true, nullptr, &guest) != 0) { *__error_nid_postfix() = GuestEagain; return -1; } + *received = guest; + return 0; } int APS5_VABI pause_nid_postfix(void) { - NotImplemented_nid_no_patch("pause: waiting for a delivered signal"); - return -1; + auto& pending = Pending(); + for (;;) { + int guest = 0; + { + std::unique_lock lock(pending.lock); + if (!TakePendingUnblocked(&guest)) { + pending.changed.wait(lock); + continue; + } + } + if (DeliverPendingSignal(guest)) { + *__error_nid_postfix() = GuestEintr; + return -1; + } + } +} + +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; } } diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c5c0f22207..672c9d2911 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -2,6 +2,9 @@ #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif #include #include #include @@ -41,6 +44,20 @@ 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 +} + } extern "C" { @@ -101,6 +118,15 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void const int value = static_cast(processors); return CopyOut(&value, sizeof(value), oldValue, oldLength); } + if (nameLength == 2 && name[0] == 6 && name[1] == 2) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + static const std::string model = HostCpuModel(); + if (model.empty()) throw std::runtime_error(std::string(__func__) + ": the host CPU has no CPUID brand string for hw.model"); + return CopyOut(model.c_str(), model.size() + 1, oldValue, oldLength); + } if (nameLength == 2 && name[0] == 6 && name[1] == 5) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -188,6 +214,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } + if (std::strcmp(name, "hw.model") == 0) { + static constexpr int mib[] = {6, 2}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "hw.physmem") == 0) { static constexpr int mib[] = {6, 5}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); diff --git a/core/libs/prx/libkernel/System/src/ThermalSensors.cpp b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp new file mode 100644 index 0000000000..992e6e468d --- /dev/null +++ b/core/libs/prx/libkernel/System/src/ThermalSensors.cpp @@ -0,0 +1,275 @@ +#include "prx/libkernel/System/include/ThermalSensors.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#endif + +namespace { + +std::optional ReadCelsius(const std::filesystem::path& file) { + std::ifstream stream(file); + long long millidegrees = 0; + if (!(stream >> millidegrees)) return std::nullopt; + if (millidegrees < -273150 || millidegrees > 2000000) return std::nullopt; + return static_cast(millidegrees / 1000); +} + +std::string ReadName(const std::filesystem::path& file) { + std::ifstream stream(file); + std::string name; + std::getline(stream, name); + return name; +} + +bool Contains(const std::string& text, const char* part) { + return text.find(part) != std::string::npos; +} + +std::string Lowercase(std::string text) { + for (char& character : text) character = static_cast(std::tolower(static_cast(character))); + return text; +} + +bool ContainsAny(const std::string& text, std::initializer_list parts) { + for (const char* part : parts) + if (text.find(part) != std::string::npos) return true; + return false; +} + +bool IsPowerOrNetwork(const std::string& name) { + if (name.rfind("bat", 0) == 0) return true; + if (name == "ac" || name.rfind("acad", 0) == 0 || name.rfind("ucsi", 0) == 0) return true; + for (const char* part : {"iwl", "mt79", "ath", "rtw", "wlan", "wifi", "wireless"}) + if (name.find(part) != std::string::npos) return true; + return false; +} + +std::filesystem::directory_iterator OpenDirectory(const std::filesystem::path& path) { + std::error_code error; + std::filesystem::directory_iterator entries(path, error); + if (error) return {}; + return entries; +} + +std::vector SortedEntries(const std::filesystem::path& directory) { + std::vector entries; + for (const auto& entry : OpenDirectory(directory)) entries.push_back(entry.path()); + std::sort(entries.begin(), entries.end()); + return entries; +} + +void Plausible(int& value) { + if (value < 0 || value > 130) value = -1; +} + +} // namespace + +extern "C" void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]) { + celsius[0] = celsius[1] = celsius[2] = -1; + if (root == nullptr) return; + const std::filesystem::path base(root); + + int gpu = -1; + int board = -1; + for (const auto& path : SortedEntries(base / "hwmon")) { + const auto value = ReadCelsius(path / "temp1_input"); + if (!value) continue; + const std::string name = Lowercase(ReadName(path / "name")); + if (IsPowerOrNetwork(name)) continue; + if (ContainsAny(name, {"coretemp", "k10temp", "zenpower", "cpu"})) { + if (celsius[0] < 0) celsius[0] = *value; + } else if (ContainsAny(name, {"amdgpu", "i915", "radeon", "nvidia", "gpu"}) || name == "xe") { + if (gpu < 0) gpu = *value; + } else if (Contains(name, "nvme")) { + if (celsius[2] < 0) celsius[2] = *value; + } else if (board < 0) { + board = *value; + } + } + for (const auto& path : SortedEntries(base / "thermal")) { + const auto value = ReadCelsius(path / "temp"); + if (!value) continue; + const std::string type = Lowercase(ReadName(path / "type")); + if (ContainsAny(type, {"x86_pkg_temp", "cpu-thermal"})) { + if (celsius[0] < 0) celsius[0] = *value; + } else if (Contains(type, "gpu")) { + if (gpu < 0) gpu = *value; + } else if (board < 0) { + board = *value; + } + } + if (celsius[0] < 0) celsius[0] = gpu; + if (celsius[1] < 0) celsius[1] = board; + Plausible(celsius[0]); + Plausible(celsius[1]); + Plausible(celsius[2]); +} + +namespace { + +#ifdef _WIN32 + +void ReadAcpiThermalZones(int celsius[2]) { + celsius[0] = celsius[1] = -1; + const HRESULT started = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + const bool uninitialize = SUCCEEDED(started); + if (FAILED(started) && started != RPC_E_CHANGED_MODE) return; + IWbemLocator* locator = nullptr; + IWbemServices* services = nullptr; + if (SUCCEEDED(CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER, IID_IWbemLocator, reinterpret_cast(&locator))) && locator != nullptr) { + BSTR path = SysAllocString(L"ROOT\\WMI"); + if (path != nullptr && SUCCEEDED(locator->ConnectServer(path, nullptr, nullptr, nullptr, 0, nullptr, nullptr, &services)) && services != nullptr) { + CoSetProxyBlanket(services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_NONE); + IEnumWbemClassObject* results = nullptr; + BSTR language = SysAllocString(L"WQL"); + BSTR query = SysAllocString(L"SELECT CurrentTemperature FROM MSAcpi_ThermalZoneTemperature"); + if (language != nullptr && query != nullptr && SUCCEEDED(services->ExecQuery(language, query, WBEM_FLAG_FORWARD_ONLY, nullptr, &results)) && results != nullptr) { + int found = 0; + while (found < 2) { + IWbemClassObject* object = nullptr; + ULONG returned = 0; + if (results->Next(1000, 1, &object, &returned) != S_OK || returned != 1 || object == nullptr) break; + VARIANT value; + VariantInit(&value); + if (SUCCEEDED(object->Get(L"CurrentTemperature", 0, &value, nullptr, nullptr))) { + if (value.vt == VT_I4 || value.vt == VT_INT) { + const long tenthsKelvin = value.vt == VT_I4 ? value.lVal : value.intVal; + int reading = static_cast(tenthsKelvin / 10) - 273; + Plausible(reading); + if (reading >= 0) celsius[found++] = reading; + } + } + VariantClear(&value); + object->Release(); + } + results->Release(); + } + if (language != nullptr) SysFreeString(language); + if (query != nullptr) SysFreeString(query); + services->Release(); + } + if (path != nullptr) SysFreeString(path); + locator->Release(); + } + if (uninitialize) CoUninitialize(); +} + +int ReadStorageTemperature() { + constexpr ULONG kStorageDeviceTemperatureProperty = 52; + constexpr ULONG kLegacyStorageDeviceTemperatureProperty = 22; + constexpr ULONG kPropertyStandardQuery = 0; + constexpr DWORD kIoctlStorageQueryProperty = 0x2D1400; + struct Information { + USHORT index; + SHORT temperature; + SHORT overThreshold; + SHORT underThreshold; + UCHAR overThresholdChangable; + UCHAR underThresholdChangable; + UCHAR eventGenerated; + UCHAR reserved0; + ULONG reserved1; + }; + struct Descriptor { + ULONG version; + ULONG size; + SHORT criticalTemperature; + SHORT warningTemperature; + USHORT infoCount; + UCHAR reserved0[2]; + ULONG reserved1[2]; + Information information[16]; + }; + constexpr std::size_t kTemperatureBytes = + offsetof(Descriptor, information) + offsetof(Information, temperature) + sizeof(SHORT); + static_assert(offsetof(Descriptor, infoCount) == 12 && offsetof(Descriptor, information) == 24 && + offsetof(Information, temperature) == 2); + struct Query { + ULONG property; + ULONG query; + UCHAR extra[1]; + }; + constexpr ULONG kProperties[] = {kStorageDeviceTemperatureProperty, kLegacyStorageDeviceTemperatureProperty}; + for (int index = 0; index < 16; ++index) { + char name[32]; + std::snprintf(name, sizeof(name), "\\\\.\\PhysicalDrive%d", index); + HANDLE handle = CreateFileA(name, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) continue; + for (ULONG property : kProperties) { + Query query{}; + query.property = property; + query.query = kPropertyStandardQuery; + Descriptor descriptor{}; + DWORD returned = 0; + const bool ok = DeviceIoControl(handle, kIoctlStorageQueryProperty, &query, sizeof(query), &descriptor, sizeof(descriptor), &returned, nullptr) != 0; + if (!ok || returned < kTemperatureBytes || descriptor.infoCount == 0) continue; + int reading = descriptor.information[0].temperature; + Plausible(reading); + if (reading >= 0) { + CloseHandle(handle); + return reading; + } + } + CloseHandle(handle); + } + return -1; +} + +#endif + +void ReadHostSensors(int celsius[3]) { +#ifdef _WIN32 + celsius[2] = ReadStorageTemperature(); + ReadAcpiThermalZones(celsius); +#elif defined(__linux__) + ReadThermalSensors_nid_no_patch("/sys/class", celsius); +#else + celsius[0] = celsius[1] = celsius[2] = -1; +#endif +} + +} + +extern "C" { + +int APS5_VABI sceKernelGetCpuTemperature(int* outCelsius) { + if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; + int celsius[3]; + ReadHostSensors(celsius); + if (celsius[0] < 0) return SCE_KERNEL_ERROR_EINVAL; + *outCelsius = celsius[0]; + return 0; +} + +int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius) { + if (outCelsius == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (sensorId < 0 || sensorId >= 3) return SCE_KERNEL_ERROR_EINVAL; + int celsius[3]; + ReadHostSensors(celsius); + if (celsius[sensorId] < 0) return SCE_KERNEL_ERROR_EINVAL; + *outCelsius = celsius[sensorId]; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4513d7604e..1432b9f07a 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -31,7 +31,9 @@ extern "C" int* APS5_VABI __error_nid_postfix(); +static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static bool IsCpuClock(int clockId) { @@ -550,6 +552,20 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { #endif } +int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp) { + if (tp == nullptr) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + if (clockId != GUEST_CLOCK_REALTIME || tp->tv_nsec < 0 || tp->tv_nsec >= 1000000000 || tp->tv_sec < 0) { + *__error_nid_postfix() = GUEST_EINVAL; + return -1; + } + char requested[48]; + std::snprintf(requested, sizeof(requested), "%lld.%09lld", static_cast(tp->tv_sec), static_cast(tp->tv_nsec)); + throw std::runtime_error(std::string("clock_settime: setting CLOCK_REALTIME to ") + requested + " is not supported, the host clock is not changed"); +} + // --------------------------------------------------------------------------- // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index eb776741a6..fbb3d6935a 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -5,16 +5,6 @@ extern "C" { -int APS5_VABI sceCoredumpWriteUserData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelGetCpuTemperature(int *out_celcius) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI __tls_get_addr_nid_postfix(void) { NotImplemented_nid_no_patch("vNe1w4diLCs"); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a50b7c0bb9..eb7ff910da 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -8,10 +8,13 @@ #include #include #include +#include #include +#include #ifdef __linux__ #include #include +#include #include #endif #include "SceTypes.hpp" @@ -25,6 +28,8 @@ #define NOMINMAX #endif #include +#include +#include #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -37,6 +42,8 @@ constexpr int GuestEnoent = 2; constexpr int GuestEperm = 1; constexpr int GuestEio = 5; constexpr int GuestEfault = 14; +constexpr int GuestEnotdir = 20; +constexpr int GuestEisdir = 21; constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; @@ -47,6 +54,52 @@ constexpr int GuestEnametoolong = 63; constexpr int GuestEnotempty = 66; constexpr int GuestEoverflow = 84; constexpr int GuestAtFdcwd = -100; +constexpr int GuestAtRemovedir = 0x800; +constexpr std::size_t GuestPathMax = 1024; +constexpr std::size_t GuestNameMax = 255; + +constexpr int PcLinkMax = 1; +constexpr int PcMaxCanon = 2; +constexpr int PcMaxInput = 3; +constexpr int PcNameMax = 4; +constexpr int PcPathMax = 5; +constexpr int PcPipeBuf = 6; +constexpr int PcChownRestricted = 7; +constexpr int PcNoTrunc = 8; +constexpr int PcVdisable = 9; +constexpr int PcAllocSizeMin = 10; +constexpr int PcFileSizeBits = 12; +constexpr int PcRecIncrXferSize = 14; +constexpr int PcRecMaxXferSize = 15; +constexpr int PcRecMinXferSize = 16; +constexpr int PcRecXferAlign = 17; +constexpr int PcSymlinkMax = 18; +constexpr int PcMinHoleSize = 21; +constexpr int PcAsyncIo = 53; +constexpr int PcPrioIo = 54; +constexpr int PcSyncIo = 55; +constexpr int PcAclExtended = 59; +constexpr int PcAclPathMax = 60; +constexpr int PcMacPresent = 63; +constexpr int PcAclNfs4 = 64; +constexpr int GuestAtSymlinkNofollow = 0x0200; + +int GuestFileError(int error) { + switch (error) { + case ENOENT: return GuestEnoent; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return GuestEinval; + case EPERM: return GuestEperm; + case EROFS: return 30; + case ENAMETOOLONG: return GuestEnametoolong; + case EIO: return GuestEio; +#ifdef ELOOP + case ELOOP: return GuestEloop; +#endif + default: return GuestEio; + } +} std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -108,6 +161,117 @@ bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& return true; } #endif + +std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string target; + if (!ReadRelativeSymbolicLink(native, target)) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + return -1; + } + return CopyLinkTarget(target, buffer, size); + } +#endif + std::error_code error; + const auto status = std::filesystem::symlink_status(native, error); + if (error) return Fail(GuestError(error)); + if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); + if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); + const auto target = std::filesystem::read_symlink(native, error).generic_string(); + if (error) return Fail(GuestError(error)); + return CopyLinkTarget(target, buffer, size); +} + +bool HasLongComponent(std::string_view path) { + for (std::size_t start = 0; start < path.size();) { + const auto end = std::min(path.find('/', start), path.size()); + if (end - start > GuestNameMax) { + if (start == 0) return true; + std::filesystem::path prefix; + if (ResolvePathError_nid_no_patch(std::string(path.substr(0, start)).c_str(), prefix)) return false; + std::error_code error; + return std::filesystem::is_directory(prefix, error); + } + start = end + 1; + } + return false; +} + +bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} + +bool IsDotOrDotDot(std::string_view path) { + while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); + const auto separator = path.find_last_of("/\\"); + const auto component = path.substr(separator == std::string_view::npos ? 0 : separator + 1); + return component == "." || component == ".."; +} + +int RenamePath(const std::filesystem::path& source, const std::filesystem::path& destination) { + namespace fs = std::filesystem; + std::error_code error; + const auto sourceStatus = fs::symlink_status(source, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + if (sourceStatus.type() == fs::file_type::not_found) + return HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent; + + const auto parent = destination.parent_path().empty() ? fs::path(".") : destination.parent_path(); + error.clear(); + const auto parentStatus = fs::status(parent, error); + if (error) return FilesystemError(error); + if (!fs::exists(parentStatus)) + return HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent; + if (!fs::is_directory(parentStatus)) return GuestEnotdir; + + error.clear(); + const auto destinationStatus = fs::symlink_status(destination, error); + if (error && error != std::errc::no_such_file_or_directory) return FilesystemError(error); + const bool destinationExists = destinationStatus.type() != fs::file_type::not_found; + const bool sourceIsDirectory = fs::is_directory(sourceStatus); + const bool destinationIsDirectory = destinationExists && fs::is_directory(destinationStatus); + bool sameFile = false; + if (destinationExists) { + error.clear(); + sameFile = fs::equivalent(source, destination, error); + if (error == std::errc::no_such_file_or_directory) error.clear(); + if (error) return FilesystemError(error); + if (!sameFile && sourceIsDirectory && !destinationIsDirectory) return GuestEnotdir; + if (!sameFile && !sourceIsDirectory && destinationIsDirectory) return GuestEisdir; + } + if (sourceIsDirectory && !sameFile) { + error.clear(); + const auto canonicalSource = fs::weakly_canonical(source, error); + if (error) return FilesystemError(error); + const auto canonicalDestination = fs::weakly_canonical(destination, error); + if (error) return FilesystemError(error); + const auto relative = canonicalDestination.lexically_relative(canonicalSource); + if (!relative.empty() && *relative.begin() != "..") return GuestEinval; + } + if (destinationExists && !sameFile && destinationIsDirectory) { + error.clear(); + if (!fs::is_empty(destination, error)) { + return error ? FilesystemError(error) : GuestEnotempty; + } + } +#ifdef _WIN32 + if (!sameFile && sourceIsDirectory && destinationIsDirectory) { + error.clear(); + if (!fs::remove(destination, error)) return error ? FilesystemError(error) : GuestEio; + } +#endif + + error.clear(); + fs::rename(source, destination, error); + return error ? FilesystemError(error) : 0; +} } extern "C" { @@ -137,25 +301,59 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); -#ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - std::string target; - if (!ReadRelativeSymbolicLink(native, target)) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); - return -1; - } - return CopyLinkTarget(target, buffer, size); - } -#endif + return ReadNativeLink(native, buffer, size); +} + +std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { + if (path == nullptr) return Fail(GuestEfault); + if (*path == '\0') return Fail(GuestEnoent); + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); + if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return Fail(error); + return ReadNativeLink(std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)), buffer, size); +} + +std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { + if (path == nullptr) return Fail(GuestEfault); + if (*path == '\0') return Fail(GuestEnoent); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return Fail(GuestEnametoolong); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); std::error_code error; - const auto status = std::filesystem::symlink_status(native, error); + const auto status = std::filesystem::status(native, error); if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); - if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); - const auto target = std::filesystem::read_symlink(native, error).generic_string(); - if (error) return Fail(GuestError(error)); - return CopyLinkTarget(target, buffer, size); + if (!std::filesystem::exists(status)) return Fail(GuestEnoent); + switch (name) { + case PcLinkMax: return 32767; + case PcNameMax: return static_cast(GuestNameMax); + case PcPathMax: return static_cast(GuestPathMax); + case PcPipeBuf: return 512; + case PcChownRestricted: return 1; + case PcNoTrunc: return 1; + case PcFileSizeBits: return 64; + case PcRecMaxXferSize: return -1; + case PcSymlinkMax: return static_cast(GuestPathMax); + case PcPrioIo: return 0; + case PcSyncIo: return 0; + case PcMaxCanon: + case PcMaxInput: + case PcVdisable: + case PcAllocSizeMin: + case PcRecIncrXferSize: + case PcRecMinXferSize: + case PcRecXferAlign: + case PcMinHoleSize: + case PcAsyncIo: + case PcAclExtended: + case PcAclPathMax: + case PcMacPresent: + case PcAclNfs4: + throw std::runtime_error("pathconf: name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); + default: return Fail(GuestEinval); + } } int APS5_VABI link_nid_postfix(const char* existing, const char* created) { @@ -190,6 +388,23 @@ int APS5_VABI fchdir_nid_postfix(int fd) { return 0; } +int APS5_VABI chroot_nid_postfix(const char* path) { + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + std::error_code error; + const auto status = std::filesystem::status(native, error); + if (status.type() == std::filesystem::file_type::not_found) return static_cast(Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent)); + if (error) return static_cast(Fail(GuestError(error))); + if (!std::filesystem::is_directory(status)) return static_cast(Fail(GuestEnotdir)); + const bool root = std::filesystem::equivalent(native, ResolvePath_nid_no_patch("/"), error); + if (error) return static_cast(Fail(GuestError(error))); + if (root) return 0; + throw std::runtime_error(std::string("chroot: changing the root directory to ") + path + " is not supported, the guest path resolver keeps a fixed root"); +} + int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { if (target == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); if (*target == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); @@ -254,4 +469,208 @@ int APS5_VABI openat_nid_postfix(int fd, const char* path, int flags, ...) { return result; } +int APS5_VABI mkdir_nid_postfix(const char* path, std::uint16_t mode); + +int APS5_VABI mkdirat_nid_postfix(int fd, const char* path, std::uint16_t mode) { + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return mkdir_nid_postfix(path, mode); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + auto native = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)); + while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); +#ifdef _WIN32 + if (!CreateDirectoryW(native.wstring().c_str(), nullptr)) { + const DWORD code = GetLastError(); + if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast(Fail(GuestEexist)); + if (code == ERROR_ACCESS_DENIED) return static_cast(Fail(GuestEperm)); + if (code == ERROR_FILENAME_EXCED_RANGE) return static_cast(Fail(GuestEnametoolong)); + if (code == ERROR_PATH_NOT_FOUND) { + if (HasNonDirectoryPrefix(native)) return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + return static_cast(Fail(GuestEio)); + } +#else + if (::mkdir(native.c_str(), static_cast(mode & 0777)) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) { + if (flag != 0 && flag != GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + std::filesystem::path native; + if (fd == GuestAtFdcwd || *path == '/') { + if (const int error = ResolvePathError_nid_no_patch(path, native)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + native = std::filesystem::path(reinterpret_cast(base)) / std::filesystem::path(reinterpret_cast(path)); + } + if (flag == GuestAtSymlinkNofollow) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + const bool symbolic = attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code status; + const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); +#endif + if (symbolic) { + NotImplemented_nid_no_patch("fchmodat: AT_SYMLINK_NOFOLLOW on a symbolic link"); + return -1; + } + } +#ifdef _WIN32 + if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(GuestFileError(errno))); +#else + if (::chmod(native.c_str(), static_cast(mode & 07777)) != 0) return static_cast(Fail(GuestFileError(errno))); +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI unlink_nid_postfix(const char* path); +int APS5_VABI rmdir_nid_postfix(const char* path); + +int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { + if (flag & ~GuestAtRemovedir) return static_cast(Fail(GuestEinval)); + if (path == nullptr) return static_cast(Fail(GuestEfault)); + if (*path == '\0') return static_cast(Fail(GuestEnoent)); + if (fd == GuestAtFdcwd || *path == '/') return (flag & GuestAtRemovedir) ? rmdir_nid_postfix(path) : unlink_nid_postfix(path); + if (std::strlen(path) >= GuestPathMax || HasLongComponent(path)) return static_cast(Fail(GuestEnametoolong)); + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); + if (base == nullptr) return static_cast(Fail(error)); + auto native = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(path)); + const bool trailingSlash = !native.has_filename() && native.has_relative_path(); + while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); + std::error_code statusError; + const auto symlinkStatus = std::filesystem::symlink_status(native, statusError); + if (flag & GuestAtRemovedir) { + if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); + if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); + if (!std::filesystem::is_directory(symlinkStatus)) { + if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + if (!std::filesystem::is_empty(native, statusError)) return static_cast(Fail(GuestEnotempty)); +#ifdef _WIN32 + struct _stat64 status{}; + const bool unlocked = ::_wstat64(native.c_str(), &status) == 0 && !(status.st_mode & _S_IWRITE) && + ::_wchmod(native.c_str(), _S_IREAD | _S_IWRITE) == 0; + if (!std::filesystem::remove(native, statusError)) { + if (unlocked) ::_wchmod(native.c_str(), _S_IREAD); + return static_cast(Fail(GuestEio)); + } +#else + if (::rmdir(native.c_str()) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + } else { + if (trailingSlash) { + if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); + if (std::filesystem::exists(symlinkStatus) || HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestEnoent)); + } + if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); +#ifdef _WIN32 + struct _stat64 status{}; + const bool unlocked = ::_wstat64(native.c_str(), &status) == 0 && (status.st_mode & _S_IFMT) == _S_IFREG && + !(status.st_mode & _S_IWRITE) && ::_wchmod(native.c_str(), _S_IREAD | _S_IWRITE) == 0; + if (::_wunlink(native.c_str()) != 0) { + const int nativeError = errno; + if (unlocked) ::_wchmod(native.c_str(), _S_IREAD); + if (HasNonDirectoryPrefix(native)) return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#else + if (::unlink(native.c_str()) != 0) { + const int nativeError = errno; + if ((nativeError == ENOENT || nativeError == ENOTDIR) && HasNonDirectoryPrefix(native)) + return static_cast(Fail(GuestEnotdir)); + return static_cast(Fail(GuestError(std::error_code(nativeError, std::generic_category())))); + } +#endif + } + RecordWrittenPath_nid_no_patch(native); + return 0; +} + +int APS5_VABI renameat_nid_postfix(int oldfd, const char* from, int newfd, const char* to) { + if (from == nullptr || to == nullptr) return static_cast(Fail(GuestEfault)); + if (*from == '\0' || *to == '\0') return static_cast(Fail(GuestEnoent)); + if ((oldfd == GuestAtFdcwd || *from == '/') && (newfd == GuestAtFdcwd || *to == '/')) + return RenameGuestPath_nid_no_patch(from, to); + if (std::strlen(from) >= GuestPathMax || HasLongComponent(from) || + std::strlen(to) >= GuestPathMax || HasLongComponent(to)) + return static_cast(Fail(GuestEnametoolong)); + if (IsDotOrDotDot(from) || IsDotOrDotDot(to)) return static_cast(Fail(GuestEinval)); + + std::filesystem::path source; + if (oldfd == GuestAtFdcwd || *from == '/') { + if (const int error = ResolvePathError_nid_no_patch(from, source)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(oldfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + source = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(from)); + } + + std::filesystem::path destination; + if (newfd == GuestAtFdcwd || *to == '/') { + if (const int error = ResolvePathError_nid_no_patch(to, destination)) return static_cast(Fail(error)); + } else { + int error = 0; + const char* base = GetDirectoryDescriptorPath_nid_no_patch(newfd, &error); + if (base == nullptr) return static_cast(Fail(error)); + destination = std::filesystem::path(reinterpret_cast(base)) / + std::filesystem::path(reinterpret_cast(to)); + } + + const std::string_view fromView(from); + const std::string_view toView(to); + const bool oldTrailingSlash = !fromView.empty() && fromView.back() == '/'; + const bool newTrailingSlash = !toView.empty() && toView.back() == '/'; + + while (!source.has_filename() && source.has_relative_path()) source = source.parent_path(); + while (!destination.has_filename() && destination.has_relative_path()) destination = destination.parent_path(); + + std::error_code statusError; + const auto sourceStatus = std::filesystem::symlink_status(source, statusError); + if (oldTrailingSlash) { + if (sourceStatus.type() == std::filesystem::file_type::not_found) + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (!std::filesystem::is_directory(sourceStatus)) return static_cast(Fail(GuestEnotdir)); + } + if (newTrailingSlash && sourceStatus.type() != std::filesystem::file_type::not_found && + !std::filesystem::is_directory(sourceStatus)) + return static_cast(Fail(GuestEnotdir)); + + const int error = RenamePath(source, destination); + if (error) return static_cast(Fail(error)); + RecordWrittenPath_nid_no_patch(source); + RecordWrittenPath_nid_no_patch(destination); + return 0; +} + } diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index a2dd29806f..43dc8405c0 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -32,6 +32,7 @@ extern "C" void __cxa_vec_delete3_nid_postfix(void*, std::size_t, std::size_t, v extern "C" _Unwind_Reason_Code _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); extern "C" _Unwind_Reason_Code _Unwind_ForcedUnwind_nid_postfix(_Unwind_Exception*, _Unwind_Stop_Fn, void*); extern "C" std::uintptr_t _Unwind_GetIP_nid_postfix(_Unwind_Context*); +extern "C" std::uintptr_t _Unwind_GetGR_nid_postfix(_Unwind_Context*, int); extern "C" void (*_ZSt13set_terminatePFvvE_nid_postfix(void(*)()))(); thread_local int destroyed = 0; @@ -263,5 +264,25 @@ int main() { return _URC_NO_REASON; }, &frames); assert(backtrace == _URC_END_OF_STACK && frames >= 2); + frames = 0; + backtrace = _Unwind_Backtrace_nid_postfix([](_Unwind_Context* context, void* argument) { + const auto* slots = *reinterpret_cast(context); + const auto validRegister = reinterpret_cast(slots[2]); + const auto getRegister = reinterpret_cast(slots[3]); + const auto setRegister = reinterpret_cast(slots[4]); + const auto validFloatRegister = reinterpret_cast(slots[5]); + const auto isSignalFrame = reinterpret_cast(slots[11]); + assert(validRegister(context, -1) && validRegister(context, -2) && validRegister(context, 16) && !validRegister(context, 17)); + assert(getRegister(context, -1) == _Unwind_GetIP_nid_postfix(context) && getRegister(context, -1) != 0); + assert(getRegister(context, -2) == _Unwind_GetGR_nid_postfix(context, 7)); + const auto rbx = getRegister(context, 3); + setRegister(context, 3, rbx + 1); + assert(_Unwind_GetGR_nid_postfix(context, 3) == rbx + 1); + setRegister(context, 3, rbx); + assert(!validFloatRegister(context, 17) && !isSignalFrame(context)); + ++*static_cast(argument); + return _URC_NO_REASON; + }, &frames); + assert(backtrace == _URC_END_OF_STACK && frames >= 2); std::puts("exception runtime tests passed"); } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index ccd01ed4a6..d463848a8a 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -6,6 +6,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif using Entry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); @@ -109,6 +112,57 @@ void RunSmallContextFirst() { } } +constexpr std::size_t HostCallContextSize = 16 * 1024; +constexpr std::size_t HostCallStack = 256 * 1024; +constexpr std::size_t HostCallFrameSize = 4096; +constexpr unsigned char HostCallGuardFill = 0x5c; +alignas(16) std::array g_hostCallArea; +FiberStorage g_hostCallStorage; + +std::size_t DescendHostStack(std::size_t remaining); +std::size_t (*volatile g_descendHostStack)(std::size_t) = DescendHostStack; + +std::size_t DescendHostStack(std::size_t remaining) { + volatile unsigned char frame[HostCallFrameSize]; + frame[0] = 0x11; + frame[HostCallFrameSize - 1] = 0x22; + const std::size_t frames = remaining > HostCallFrameSize ? g_descendHostStack(remaining - HostCallFrameSize) : 0; + Require(frame[0] == 0x11 && frame[HostCallFrameSize - 1] == 0x22, "host call frame intact"); + return frames + 1; +} + +#ifdef _WIN32 +void CheckWindowsHostStack() { + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto low = reinterpret_cast(tib->StackLimit); + const auto high = reinterpret_cast(tib->StackBase); + const volatile unsigned char marker = 0; + const auto frame = reinterpret_cast(&marker); + Require(frame > low && frame < high && high - low >= HostCallContextSize + HostCallStack, "the thread's stack bounds are the fiber's host stack"); +} +#endif + +void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { +#ifdef _WIN32 + CheckWindowsHostStack(); +#endif + Require(g_descendHostStack(HostCallStack) == HostCallStack / HostCallFrameSize, "host call depth"); + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + +void RunHostCallOnSmallContext() { + auto* fiber = reinterpret_cast(&g_hostCallStorage); + auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize; + g_hostCallArea.fill(HostCallGuardFill); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "host call", HostCallEntry, 0, context, HostCallContextSize, nullptr, 0) == 0, "initialize host call fiber"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run a 256 KiB host call from a 16 KiB fiber"); + Require(sceFiberFinalize(fiber) == 0, "finalize host call fiber"); + for (auto* byte = g_hostCallArea.data(); byte != context; ++byte) { + Require(*byte == HostCallGuardFill, "a host call from a 16 KiB fiber writes nothing below its context"); + } +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -154,4 +208,6 @@ int main() { Require(info.size_context_margin > 0 && info.size_context_margin < g_checkedContext.size() - 4096, "margin below the used stack"); Require(info.size_context_margin % 8 == 0, "margin in whole words"); Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); + + RunHostCallOnSmallContext(); } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 1b2fdf7b3b..8b2260536d 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include @@ -111,6 +112,10 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "[%ls]", static_cast(nullptr)) == 8 && std::strcmp(output, "[(null)]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "a%lcb%C", 0, 0) == 4 && std::memcmp(output, "a\0b\0", 5) == 0); Require(FormatList(output, sizeof(output), "%%ls:%2$d:%1$d", 3, 7) == 7 && std::strcmp(output, "%ls:7:3") == 0); + Require(FormatList(output, sizeof(output), "%ls[%f][%+f][% F][%5.1e]", u"", -NAN, -NAN, NAN, -NAN) == 22 + && std::strcmp(output, "[nan][nan][NAN][ nan]") == 0); + Require(FormatList(output, sizeof(output), "%ls[%-6g][%06a][%LG][%+f]", u"", NAN, -NAN, -static_cast(NAN), -INFINITY) == 27 + && std::strcmp(output, "[nan ][ nan][NAN][-inf]") == 0); for (std::size_t capacity = 0; capacity <= 6; ++capacity) { std::memset(output, '!', sizeof(output)); int count = -1; diff --git a/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp new file mode 100644 index 0000000000..89ca36d6f2 --- /dev/null +++ b/core/libs/tests/GuestAgcDriverDebugUnavailable.cpp @@ -0,0 +1,38 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceAgcDriverGetShaderDebuggingStatus(); +int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(std::uint32_t); +int APS5_VABI sceAgcDriverSetSubmitValidationMode(std::uint32_t); +int APS5_VABI sceAgcDriverGetSubmitValidationMode(std::uint32_t*); +int APS5_VABI sceAgcDriverSetSubmitValidationConfig(const void*); +int APS5_VABI sceAgcDriverGetSubmitValidationConfig(void*); +} + +static constexpr int DebugUnavailable = static_cast(0x8A6C1000); +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + Require(sceAgcDriverGetShaderDebuggingStatus() == 1); + + Require(sceAgcDriverSetValidationErrorOutputFrequency(0u) == DebugUnavailable); + Require(sceAgcDriverSetValidationErrorOutputFrequency(60u) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationMode(0u) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationMode(1u) == DebugUnavailable); + + std::uint32_t mode = 0xA5A5A5A5u; + Require(sceAgcDriverGetSubmitValidationMode(&mode) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationMode(nullptr) == DebugUnavailable); + Require(mode == 0xA5A5A5A5u); + + std::array config{}; + config.fill(0x5A); + Require(sceAgcDriverSetSubmitValidationConfig(config.data()) == DebugUnavailable); + Require(sceAgcDriverSetSubmitValidationConfig(nullptr) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationConfig(config.data()) == DebugUnavailable); + Require(sceAgcDriverGetSubmitValidationConfig(nullptr) == DebugUnavailable); + for (const std::uint8_t byte : config) Require(byte == 0x5A); +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 37bc4c9dff..2845913f5c 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -82,6 +82,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void*, std::uin std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void*, std::uint64_t, std::uint64_t); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState(); std::size_t APS5_VABI sceKernelGetDirectMemorySize(); +int APS5_VABI sceAmprAmmGetUsageStatsData(void*); int APS5_VABI sceAmprAmmCommandBufferConstructor(Apr::CommandBufferObject*); int APS5_VABI sceAmprAmmCommandBufferMap(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t); int APS5_VABI sceAmprAmmCommandBufferMapWithGpuMaskId(Apr::CommandBufferObject*, std::uint64_t, std::uint64_t, std::int32_t, std::int32_t, std::uint8_t); @@ -263,6 +264,36 @@ void TestFullBuffer() { Require(sceAmprCommandBufferPopMarker(&unbound) == bufferFull); } +void TestOffsetPastBuffer() { + constexpr std::array invalidOffsets{33u, 64u, 0xffffffffu}; + for (const auto offset : invalidOffsets) { + Recorder recorder(32); + recorder.memory.fill(0xA5); + recorder.buffer.offset = offset; + recorder.buffer.numCommands = 7; + const auto before = recorder.buffer; + const auto memory = recorder.memory; + const auto rejected = [&](const auto& record) { + bool threw = false; + try { + record(); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(recorder.memory == memory); + Require(recorder.buffer.base == before.base && recorder.buffer.size == before.size); + Require(recorder.buffer.offset == before.offset && recorder.buffer.numCommands == before.numCommands); + Require(recorder.buffer.type == before.type && recorder.buffer.recording == before.recording); + }; + rejected([&] { sceAmprCommandBufferPopMarker(&recorder.buffer); }); + rejected([&] { sceAmprCommandBufferNop(&recorder.buffer, 1); }); + rejected([&] { sceAmprCommandBufferPushMarker(&recorder.buffer, "bounds"); }); + rejected([&] { sceAmprAmmCommandBufferMap(&recorder.buffer, 0x4000, 0x4000, 0, 3); }); + rejected([&] { sceAmprAmmCommandBufferMapAsPrt(&recorder.buffer, 0x4000, 0x4000); }); + } +} + void TestSubmission() { Recorder recorder; std::uint64_t first = 0; @@ -823,6 +854,24 @@ void TestAmmRangesAboveHintedMaps() { Require(sceKernelMunmap(span, page) == 0); } +void TestAmmUsageStats() { + std::array stats{}; + Require(sceAmprAmmGetUsageStatsData(nullptr) == static_cast(0x80020016)); + stats[0] = 0x19; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == static_cast(0x80020016)); + stats = {0x18, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0x18); + Require((stats[1] & 0xffffu) == 512u && (stats[1] >> 16u) == 0u); + Require((stats[2] & 0xffffffffu) == 7u && (stats[2] >> 32u) == 0u); + stats = {0x8, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0x8 && stats[1] == 0xdeadbeef && stats[2] == 0xdeadbeef); + stats = {0x0, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; + Require(sceAmprAmmGetUsageStatsData(stats.data()) == 0); + Require(stats[0] == 0 && stats[1] == 0xdeadbeef); +} + void TestAmm() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -964,6 +1013,102 @@ void SubmitAmm(Recorder& recorder) { Require(sceAmprAmmWaitCommandBufferCompletion(id) == 0); } +void TestAmmRemapKeepsPoolReferences() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), page, page, 1, &pool) == 0); + + const auto source = start + 256 * page; + const auto alias = multimapStart + 256 * page; + const auto fresh = start + 257 * page; + const auto lastAlias = multimapStart + 257 * page; + const auto overlappingAlias = multimapStart + 258 * page; + Recorder maps; + Require(sceAmprAmmCommandBufferMap(&maps.buffer, source, page, 0, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMultiMap(&maps.buffer, alias, source, page, cpuReadWrite) == 0); + SubmitAmm(maps); + At(source) = 0xA1; + Require(At(alias) == 0xA1); + + Recorder move; + Require(sceAmprAmmCommandBufferRemap(&move.buffer, alias, source, page, cpuReadWrite) == 0); + SubmitAmm(move); + Require(Protection(source) == 0 && At(alias) == 0xA1); + + Recorder exhausted; + Require(sceAmprAmmCommandBufferMap(&exhausted.buffer, fresh, page, 0, cpuReadWrite) == 0); + const auto requireExhausted = [&] { + bool rejected = false; + try { + SubmitAmm(exhausted); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("AMM: no free run of "); + } + Require(rejected); + }; + requireExhausted(); + At(alias) = 0xB1; + Require(At(alias) == 0xB1); + + Recorder retain; + Require(sceAmprAmmCommandBufferRemap(&retain.buffer, alias, alias, page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferMultiMap(&retain.buffer, lastAlias, alias, page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferRemap(&retain.buffer, lastAlias, alias, 2 * page, cpuReadWrite) == 0); + Require(sceAmprAmmCommandBufferUnmap(&retain.buffer, lastAlias, page) == 0); + SubmitAmm(retain); + Require(At(overlappingAlias) == 0xB1); + requireExhausted(); + + Recorder release; + Require(sceAmprAmmCommandBufferUnmap(&release.buffer, overlappingAlias, page) == 0); + Require(sceAmprAmmCommandBufferMap(&release.buffer, fresh, page, 0, cpuReadWrite) == 0); + SubmitAmm(release); + Require(At(fresh) == 0xB1); + At(fresh) = 0xC1; + Require(At(fresh) == 0xC1); +} + +void TestAmmMapFailureKeepsPoolOwnership() { + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0); + std::int64_t pool = -1; + Require(sceAmprAmmGiveDirectMemory(0, static_cast(sceKernelGetDirectMemorySize()), page, page, 1, &pool) == 0); + const auto original = start + 512 * page; + const auto fresh = start + 516 * page; + + Recorder mapping; + Require(sceAmprAmmCommandBufferMap(&mapping.buffer, original, page, 0, cpuReadWrite) == 0); + SubmitAmm(mapping); + At(original) = 0xA1; + SubmitAmm(mapping); + Require(At(original) == 0xA1 && Protection(original) == cpuReadWrite); + + Recorder oversized; + Require(sceAmprAmmCommandBufferMap(&oversized.buffer, original, 2 * page, 0, cpuReadWrite) == 0); + bool rejected = false; + try { + SubmitAmm(oversized); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("AMM: no free run of "); + } + Require(rejected); + Require(Protection(original) == 0 && Protection(original + page) == 0); + + Recorder reuse; + Require(sceAmprAmmCommandBufferMap(&reuse.buffer, fresh, page, 0, cpuReadWrite) == 0); + SubmitAmm(reuse); + Require(At(fresh) == 0xA1); + At(fresh) = 0xB1; + Require(At(fresh) == 0xB1 && Protection(original) == 0); +} + void TestAmmRemapAndProtect() { std::uint64_t start = 0; std::uint64_t end = 0; @@ -1244,6 +1389,7 @@ int main() { TestRecording(std::string(300, 'a')); TestRejectedArguments(); TestFullBuffer(); + TestOffsetPastBuffer(); TestSubmission(); TestKernelEventQueue(); TestWaits(); @@ -1262,6 +1408,9 @@ int main() { TestResolveSymbolicLink(); TestGatherScatter(); TestAmmRangesAboveHintedMaps(); + TestAmmUsageStats(); + TestAmmRemapKeepsPoolReferences(); + TestAmmMapFailureKeepsPoolOwnership(); TestAmm(); TestAmmRemapAndProtect(); TestAmmPrt(); diff --git a/core/libs/tests/GuestAmprCppError.S b/core/libs/tests/GuestAmprCppError.S new file mode 100644 index 0000000000..024758cd41 --- /dev/null +++ b/core/libs/tests/GuestAmprCppError.S @@ -0,0 +1,57 @@ +.cfi_sections .eh_frame +.text +.globl GuestCatch +GuestCatch: + .cfi_startproc + .cfi_personality 0x00, __gxx_personality_v0_nid_postfix + .cfi_lsda 0x00, .Lcatch_lsda + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset %rbp, -16 + movq %rsp, %rbp + .cfi_def_cfa_register %rbp + subq $16, %rsp +.Lcatch_call: + call _ZSt16_Throw_Cpp_errori_nid_postfix +.Lcatch_call_end: + ud2 +.Lcatch_landing: + cmpq $2, %rdx + jne .Lcatch_bad + movq %rax, %rdi + call __cxa_begin_catch_nid_postfix + movq %rax, %rdi + call FixtureInspect + call __cxa_end_catch_nid_postfix + leave + .cfi_def_cfa %rsp, 8 + ret +.Lcatch_bad: + movl $1, %edi + call FixtureFail + ud2 + .cfi_endproc +#ifdef _WIN32 +.section .rdata,"dr" +#else +.section .rodata +#endif +.Lcatch_lsda: + .byte 0xff, 0x80 + .uleb128 .Lcatch_types_end-.Lcatch_types_offset +.Lcatch_types_offset: + .byte 0x01 + .uleb128 .Lcatch_calls_end-.Lcatch_calls +.Lcatch_calls: + .uleb128 .Lcatch_call-GuestCatch + .uleb128 .Lcatch_call_end-.Lcatch_call + .uleb128 .Lcatch_landing-GuestCatch + .uleb128 1 +.Lcatch_calls_end: + .byte 1, 1, 2, 0 + .quad FixtureCatchType + .quad FixtureOtherType +.Lcatch_types_end: +#ifndef _WIN32 +.section .note.GNU-stack,"",@progbits +#endif diff --git a/core/libs/tests/GuestAmprCppError.cpp b/core/libs/tests/GuestAmprCppError.cpp new file mode 100644 index 0000000000..8e2407c199 --- /dev/null +++ b/core/libs/tests/GuestAmprCppError.cpp @@ -0,0 +1,162 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct Category { const void* const* vtable; }; +struct ErrorCode { int value; const Category* category; }; +struct SystemError { const void* const* vtable; const char* message; ErrorCode code; }; +static_assert(sizeof(SystemError) == 32 && offsetof(SystemError, code) == 16); + +using GuestWhat = const char* (APS5_VABI *)(const SystemError*); +using CategoryDelete = void (APS5_VABI *)(const Category*); +using CategoryName = const char* (APS5_VABI *)(const Category*); +using CategoryMessage = void* (APS5_VABI *)(void*, const Category*, int); +using CategoryCondition = ErrorCode (APS5_VABI *)(const Category*, int); +using CategoryEquivalentCondition = bool (APS5_VABI *)(const Category*, int, const ErrorCode*); +using CategoryEquivalentCode = bool (APS5_VABI *)(const Category*, const ErrorCode*, int); + +struct Expected { int value; const char* what; }; +static constexpr Expected Errors[] { + {16, "device or resource busy: device or resource busy"}, + {22, "invalid argument: invalid argument"}, + {3, "no such process: no such process"}, + {12, "not enough memory: not enough memory"}, + {1, "operation not permitted: operation not permitted"}, + {11, "resource deadlock would occur: resource deadlock would occur"}, + {35, "resource unavailable try again: operation would block"} +}; + +extern "C" { +extern const unsigned char _ZTISt9exception_nid_postfix[]; +extern const unsigned char _ZTISt11logic_error_nid_postfix[]; +extern const unsigned char _ZTISt13runtime_error_nid_postfix[]; +extern const unsigned char _ZTISt12system_error_nid_postfix[]; +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code); +void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(SystemError* self, const SystemError* source); +SystemError* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(SystemError* self, const SystemError* source); +void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(SystemError* self); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +void APS5_VABI GuestCatch(int code); + +const void* FixtureCatchType = nullptr; +const void* FixtureOtherType = nullptr; +const Expected* FixtureExpected = nullptr; +unsigned FixtureInspected = 0; + +[[noreturn]] void APS5_VABI FixtureFail(unsigned code) { + std::fprintf(stderr, "guest _Throw_Cpp_error failure %u\n", code); + std::exit(static_cast(code)); +} +} + +static void Require(bool value, unsigned code) { + if (!value) FixtureFail(code); +} + +template +static TFunction Slot(const void* const* vtable, int index) { + return reinterpret_cast(const_cast(vtable[index])); +} + +static const TypeRecord* TypeOf(const void* const* vtable, int destroySlot) { + return static_cast(vtable[destroySlot - 1]); +} + +static void CheckCategory(const SystemError& error) { + const Category* category = error.code.category; + Require(category != nullptr, 20); + const TypeRecord* type = TypeOf(category->vtable, 0); + Require(std::strcmp(type->name, "St23_Generic_error_category") == 0, 21); + Require(type->base && std::strcmp(type->base->name, "St14error_category") == 0, 22); + Require(std::strcmp(Slot(category->vtable, 2)(category), "generic") == 0, 23); + const ErrorCode condition = Slot(category->vtable, 4)(category, error.code.value); + Require(condition.value == error.code.value && condition.category == category, 24); + const ErrorCode other {error.code.value + 1, category}; + const ErrorCode foreign {error.code.value, nullptr}; + auto equivalentCondition = Slot(category->vtable, 5); + Require(equivalentCondition(category, error.code.value, &condition), 25); + Require(!equivalentCondition(category, error.code.value, &other), 26); + Require(!equivalentCondition(category, error.code.value, &foreign), 27); + auto equivalentCode = Slot(category->vtable, 6); + Require(equivalentCode(category, &error.code, error.code.value), 28); + Require(!equivalentCode(category, &error.code, error.code.value + 1), 29); + Require(!equivalentCode(category, &foreign, error.code.value), 30); + GuestLocale::String result {}; + Require(Slot(category->vtable, 3)(&result, category, error.code.value) == &result, 33); + const char* data = result.capacity <= 15 ? result.buffer : result.pointer; + Require(std::strcmp(data, std::strstr(FixtureExpected->what, ": ") + 2) == 0, 34); + if (result.capacity > 15) _ZdlPv_nid_postfix(result.pointer); + bool deleteThrew = false; + try { + Slot(category->vtable, 1)(category); + } catch (const std::invalid_argument&) { + deleteThrew = true; + } + Require(deleteThrew, 32); +} + +static void CheckCopies(const SystemError& error) { + SystemError copy {}; + _ZNSt12system_errorC1ERKS__nid_postfix(©, &error); + Require(copy.vtable == error.vtable && copy.message == error.message, 40); + Require(copy.code.value == error.code.value && copy.code.category == error.code.category, 41); + SystemError assigned {}; + _ZNSt12system_errorC1ERKS__nid_postfix(&assigned, &error); + assigned.code = {0, nullptr}; + Require(_ZNSt12system_erroraSERKS__nid_postfix(&assigned, &error) == &assigned, 42); + Require(assigned.code.value == error.code.value && assigned.code.category == error.code.category, 43); + _ZNSt12system_errorD1Ev_nid_postfix(&assigned); + _ZNSt12system_errorD1Ev_nid_postfix(©); + Require(copy.message == nullptr && assigned.message == nullptr, 44); +} + +extern "C" void APS5_VABI FixtureInspect(void* object) { + const auto& error = *static_cast(object); + Require(std::strcmp(TypeOf(error.vtable, 0)->name, "St12system_error") == 0, 10); + const char* what = Slot(error.vtable, 2)(&error); + Require(what && std::strcmp(what, FixtureExpected->what) == 0, 11); + Require(error.code.value == FixtureExpected->value, 12); + CheckCategory(error); + CheckCopies(error); + ++FixtureInspected; +} + +static void Catch(const void* type, int code) { + FixtureCatchType = type; + FixtureExpected = &Errors[code]; + const auto before = FixtureInspected; + GuestCatch(code); + Require(FixtureInspected == before + 1, 2); +} + +static void CheckOutOfRange(int code) { + bool threw = false; + try { + _ZSt16_Throw_Cpp_errori_nid_postfix(code); + } catch (const std::out_of_range& error) { + threw = std::string(error.what()).find(std::to_string(code)) != std::string::npos; + } + Require(threw, 3); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + FixtureOtherType = _ZTISt11logic_error_nid_postfix; + for (int code = 0; code < static_cast(std::size(Errors)); ++code) Catch(_ZTISt12system_error_nid_postfix, code); + Catch(_ZTISt13runtime_error_nid_postfix, 5); + Catch(_ZTISt9exception_nid_postfix, 6); + CheckOutOfRange(-1); + CheckOutOfRange(static_cast(std::size(Errors))); + std::puts("guest _Throw_Cpp_error system_error, generic category and range: PASS"); + return 0; +} diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 6fa491548f..ef98b12fed 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -10,13 +10,51 @@ #include #include #include +#ifndef _WIN32 +#include +#include +#endif struct GuestXtime { std::int64_t sec; std::int64_t nsec; }; +struct PadTarget { + std::atomic* order; + std::atomic* destroyed; + int beforeRelease; + int afterRelease; + unsigned exitCode; +}; + +struct GuestTypeInfo { + const void* vtable; + const char* name; +}; + +struct GuestSystemError { + const void* vtable; + const char* message; + int code; + const void* category; +}; + +using GuestTerminate = void (APS5_VABI*)(); + +struct TestPad { + unsigned (APS5_VABI* const* vtable)(TestPad*); + void* condition; + void* mutex; + bool started; + PadTarget target; +}; + extern "C" { +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(TestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(TestPad* pad, Pthread* thread); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(TestPad* pad); int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); @@ -35,10 +73,14 @@ int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +GuestTerminate APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(GuestTerminate handler); +const GuestTypeInfo* APS5_VABI __cxa_current_exception_type_nid_postfix(); +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); } static constexpr int ThrdSuccess = 0; static constexpr int ThrdError = 4; +static constexpr int GuestEbusy = 16; static constexpr int GuestEinval = 22; static constexpr int GuestErange = 34; static constexpr int GuestEilseq = 86; @@ -316,7 +358,181 @@ static void TestConditionSignal() { _Mtx_destroy_nid_postfix(&context.mutex); } +static unsigned APS5_VABI PadGo(TestPad* pad) { + const auto local = pad->target; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static constexpr std::array PadVtable{PadGo}; + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TException&) { + return true; + } + return false; +} + +static void ConstructPad(TestPad& pad, std::atomic& order, std::atomic& destroyed, int index, unsigned exitCode) { + std::memset(&pad, 0xCD, sizeof(pad)); + pad.vtable = PadVtable.data(); + _ZNSt4_PadC2Ev_nid_postfix(&pad); + Require(pad.vtable == PadVtable.data()); + Require(pad.condition && pad.mutex && !pad.started); + pad.target = {&order, &destroyed, 2 * index + 1, 2 * index + 2, exitCode}; +} + +static void TestPadLaunch() { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr); + Require(pad.started); + Require(order.load() == 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(order.load() == 1); + destroyed = true; + int code = -1; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(code == 0); + Require(order.load() == 2); +} + +static void TestPadLaunchMany() { + constexpr int Count = 4; + std::array, Count> orders{}; + std::atomic destroyed{false}; + std::array pads{}; + std::array threads{}; + for (int index = 0; index < Count; ++index) { + ConstructPad(pads[index], orders[index], destroyed, index, 100u + index); + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pads[index], &threads[index]); + Require(orders[index].load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pads[index]); + } + destroyed = true; + for (int index = 0; index < Count; ++index) { + int code = 0; + Require(_Thrd_join_nid_postfix(threads[index], &code) == ThrdSuccess); + Require(code == 100 + index); + Require(orders[index].load() == 2 * index + 2); + } +} + +static unsigned APS5_VABI PadGoImmediate(TestPad* pad) { + const auto local = pad->target; + local.order->store(local.beforeRelease); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(pad); + while (!local.destroyed->load()) std::this_thread::yield(); + local.order->store(local.afterRelease); + return local.exitCode; +} + +static void TestPadConcurrentLaunch() { + constexpr int Count = 4; + constexpr std::array immediateVtable{PadGoImmediate}; + std::atomic ready{0}; + std::array parents; + for (int index = 0; index < Count; ++index) { + parents[index] = std::thread([&, index] { + std::atomic order{0}; + std::atomic destroyed{false}; + TestPad pad; + ConstructPad(pad, order, destroyed, index, 0x80000000u + index); + pad.vtable = immediateVtable.data(); + ready.fetch_add(1); + while (ready.load() != Count) std::this_thread::yield(); + Pthread thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, &thread); + Require(thread != nullptr && pad.started && order.load() == 2 * index + 1); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + std::memset(&pad, 0xcd, sizeof(pad)); + destroyed = true; + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == ThrdSuccess); + Require(static_cast(code) == 0x80000000u + index); + Require(order.load() == 2 * index + 2); + }); + } + for (auto& parent : parents) parent.join(); +} + +struct ForeignDestroy { + TestPad* pad; + bool rejected; +}; + +static void* APS5_VABI DestroyPad(void* arg) { + auto& context = *static_cast(arg); + context.rejected = Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(context.pad); }); + return nullptr; +} + +#ifndef _WIN32 +static void APS5_VABI ExitWithSystemErrorCode() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + _exit(type && error && std::strcmp(type->name, "St12system_error") == 0 ? error->code : 1); +} + +template +static int GuestSystemErrorCode(TCall call) { + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + _ZSt13set_terminatePFvvE_nid_postfix(ExitWithSystemErrorCode); + call(); + _exit(2); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) ? WEXITSTATUS(status) : -1; +} +#endif + +static void TestPadErrors() { + Require(Throws([] { _ZNSt4_PadC2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_PadD2Ev_nid_postfix(nullptr); })); + Require(Throws([] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(nullptr); })); + Pthread thread = nullptr; + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(nullptr, &thread); })); + + std::atomic order{0}; + std::atomic destroyed{true}; + TestPad pad; + ConstructPad(pad, order, destroyed, 0, 0); + Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, nullptr); })); + Require(!pad.started && order.load() == 0); +#ifndef _WIN32 + Require(GuestSystemErrorCode([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); +#endif + Require(!pad.started); + + ForeignDestroy context{&pad, false}; + Require(_Thrd_join_nid_postfix(Start(DestroyPad, &context), nullptr) == ThrdSuccess); + Require(context.rejected); + Require(pad.condition && pad.mutex); + _ZNSt4_PadD2Ev_nid_postfix(&pad); + Require(!pad.condition && !pad.mutex); + Require(Throws([&] { _ZNSt4_PadD2Ev_nid_postfix(&pad); })); +} + int main() { + TestPadLaunch(); + TestPadLaunchMany(); + TestPadConcurrentLaunch(); + TestPadErrors(); TestThreadJoin(); TestThreadSleep(); TestSpinLock(); diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 6780ff8140..1f68eaec06 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -10,6 +10,7 @@ extern "C" { int APS5_VABI sceAppContentInitialize(const AppContentInitParam*, AppContentBootParam*); 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 sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -19,6 +20,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*); int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint*, size_t*); +int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint*); int APS5_VABI sceAppContentAppParamGetInt(uint32_t, int32_t*); } @@ -47,6 +49,16 @@ int main() { Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, &availableKb) == 0); Require(availableKb == 0); Require(sceAppContentDownloadDataGetAvailableSpaceKb(&download, nullptr) == ErrorParameter); + const auto downloadData = ResolvePath_nid_no_patch("/download0"); + Require(sceAppContentDownloadDataFormat(&download) == 0); + Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); + std::filesystem::create_directory(downloadData / "cache"); + { std::ofstream file(downloadData / "cache" / "old.bin"); file << "downloaded"; } + { std::ofstream file(downloadData / "index.dat"); file << "downloaded"; } + Require(sceAppContentDownloadDataFormat(nullptr) == ErrorParameter); + Require(std::filesystem::exists(downloadData / "index.dat")); + Require(sceAppContentDownloadDataFormat(&download) == 0); + Require(std::filesystem::is_directory(downloadData) && std::filesystem::is_empty(downloadData)); const int32_t expectedParams[] = {3, 23566, -7, 0, 2147483647}; for (uint32_t id = 0; id < 5; ++id) { @@ -71,6 +83,10 @@ int main() { std::memcpy(mountPoint.data, "/addcont0", 10); Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound); Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); + Require(sceAppContentAddcontEnqueueDownload(0, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); + Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == ErrorParameter); + Require(sceAppContentAddcontMount(0, &label, &mountPoint) == ErrorNotFound); std::filesystem::create_directory(directory / "temp0"); { std::ofstream file(directory / "temp0" / "old.txt"); file << "previous run"; } diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index cf40f562b5..76b09054a6 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -1,9 +1,13 @@ #include "SceTypes.hpp" +#include #include #include #include #include #include +#include +#include +#include extern "C" { KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); @@ -22,6 +26,8 @@ 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); } static constexpr int SCE_OK = 0; @@ -40,6 +46,8 @@ 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; @@ -60,6 +68,13 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } +static void* APS5_VABI RecordThreadId(void* arg) { + auto* data = static_cast*, std::future*>*>(arg); + data->first->store(scePthreadGetthreadid(), std::memory_order_release); + data->second->get(); + return nullptr; +} + static void* APS5_VABI ReportCpuset(void* arg) { auto* result = static_cast(arg); std::uint64_t set[4] = {~0ull, ~0ull, ~0ull, ~0ull}; @@ -223,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); @@ -232,4 +280,47 @@ int main() { Require(pthread_getaffinity_np_nid_postfix(nullptr, GUEST_CPUSET_BYTES, rawSet) == GUEST_EINVAL); Require(pthread_getaffinity_np_nid_postfix(nullptr, 0, rawSet) == GUEST_EINVAL); Require(pthread_getaffinity_np_nid_postfix(pthread_self_nid_postfix(), GUEST_CPUSET_BYTES, nullptr) == GUEST_EFAULT); + Require(cpuset_getaffinity_nid_postfix(3, 1, scePthreadGetthreadid(), 8, processSet) == 0 && processSet[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(3, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(99, 1, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_getaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_getaffinity_nid_postfix(3, 99, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(3, 1, scePthreadGetthreadid(), 8, processSet) == 0); + Require(cpuset_setaffinity_nid_postfix(3, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(3, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(99, 1, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(99, 1, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(99, 2, 999999, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(3, 99, -1, 8, processSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + const auto throws = [](auto&& call) { + try { + call(); + return false; + } catch (const std::runtime_error&) { + return true; + } + }; + Require(throws([&] { cpuset_getaffinity_nid_postfix(1, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(2, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 3, -1, 8, processSet); })); + Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(1, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(2, 1, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 3, -1, 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 4, -1, 8, processSet); })); + std::promise holdOther; + auto heldOther = holdOther.get_future(); + std::atomic otherTid{0}; + Pthread otherThread = nullptr; + std::pair otherArg{&otherTid, &heldOther}; + Require(scePthreadCreate(&otherThread, nullptr, RecordThreadId, &otherArg, nullptr) == SCE_OK); + while (otherTid.load(std::memory_order_acquire) == 0) std::this_thread::yield(); + Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet); })); + Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet); })); + holdOther.set_value(); + Require(scePthreadJoin(otherThread, nullptr) == SCE_OK); + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); } diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp new file mode 100644 index 0000000000..4be35302dc --- /dev/null +++ b/core/libs/tests/GuestCallTrace.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Block { + std::uint64_t words[4]; +}; + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Call trace check failed at line %u\n", location.line()); + std::abort(); + } +} + +static std::uint64_t APS5_VABI Integers(std::uint64_t a, std::uint64_t b, std::uint64_t c, std::uint64_t d, std::uint64_t e, std::uint64_t f, std::uint64_t g, std::uint64_t h) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h; +} + +static double APS5_VABI Doubles(double a, double b, double c, double d, double e, double f, double g, double h, double i, double j) { + return a + 2 * b + 3 * c + 4 * d + 5 * e + 6 * f + 7 * g + 8 * h + 9 * i + 10 * j; +} + +static double APS5_VABI Mixed(int a, double b, const char* c, float d, std::uint64_t e) { + return a + b + static_cast(std::strlen(c)) + d + static_cast(e); +} + +static double APS5_VABI Variadic(int count, ...) { + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, count); + double sum = 0; + for (int index = 0; index < count; ++index) { + sum += static_cast(__builtin_va_arg(arguments, std::uint64_t)) * (index + 1); + sum += __builtin_va_arg(arguments, double) * (index + 1); + } + __builtin_sysv_va_end(arguments); + return sum; +} + +static Block APS5_VABI Blocks(std::uint64_t seed) { + return {{seed, seed + 1, seed + 2, seed + 3}}; +} + +template +static TFunction* Traced(TFunction* function, const char* name, const char* library) { + return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); +} + +int main(int argc, char** argv) { + Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); + if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { + const auto first = Traced(&Integers, "Integers", "libTraced.prx"); + const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); + Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + std::abort(); + } + CallTraceEntry entries[16]; + CallTraceCount counts[8]; + std::uint64_t total = 1; + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 0 && total == 0); + Require(Traced(&Integers, "Integers", "libUnselected.prx") == &Integers); + Require(TraceImport_nid_no_patch(nullptr, "Integers", "libTraced.prx") == nullptr); + Require(CopyCallTrace_nid_no_patch(entries, 16) == 0); + + const auto integers = Traced(&Integers, "Integers", "libTraced.prx"); + const auto doubles = Traced(&Doubles, "Doubles", "libTraced"); + const auto mixed = Traced(&Mixed, "Mixed", "libOther.prx"); + const auto variadic = Traced(&Variadic, "Variadic", "libOther"); + const auto blocks = Traced(&Blocks, "Blocks", "libTraced.prx"); + Require(integers != &Integers && doubles != &Doubles && mixed != &Mixed && variadic != &Variadic && blocks != &Blocks); + Require(Traced(&Integers, "Alias", "libOther.prx") == integers); + + Require(integers(1, 2, 3, 4, 5, 6, 7, 8) == Integers(1, 2, 3, 4, 5, 6, 7, 8)); + Require(doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5) == Doubles(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5)); + Require(mixed(-3, 0.25, "trace", 1.5f, 1ull << 40) == Mixed(-3, 0.25, "trace", 1.5f, 1ull << 40)); + const double expected = Variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5); + Require(variadic(8, 1ull, 0.5, 2ull, 1.5, 3ull, 2.5, 4ull, 3.5, 5ull, 4.5, 6ull, 5.5, 7ull, 6.5, 8ull, 7.5) == expected); + const Block block = blocks(40); + Require(block.words[0] == 40 && block.words[1] == 41 && block.words[2] == 42 && block.words[3] == 43); + + const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; + const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; + MEMORY_BASIC_INFORMATION image{}; + Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); + for (std::uint64_t index = 0; index < 5; ++index) { + Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); + MEMORY_BASIC_INFORMATION caller{}; + Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); + Require(caller.AllocationBase == image.AllocationBase); + } + Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); + Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); + for (std::size_t index = 0; index < 5; ++index) { + Require(counts[index].calls == 1 && std::strcmp(counts[index].name, names[index]) == 0 && std::strcmp(counts[index].library, libraries[index]) == 0); + } + Require(doubles(1, 1, 1, 1, 1, 1, 1, 1, 1, 1) == 55 && blocks(1).words[0] == 1 && blocks(2).words[0] == 2); + Require(CopyCallCounts_nid_no_patch(counts, 2, nullptr) == 2); + Require(counts[0].calls == 3 && std::strcmp(counts[0].name, "Blocks") == 0 && counts[1].calls == 2 && std::strcmp(counts[1].name, "Doubles") == 0); + Require(CopyCallCounts_nid_no_patch(nullptr, 8, &total) == 0 && total == 8); + + constexpr std::uint64_t Workers = 4; + constexpr std::uint64_t Calls = 20000; + std::vector workers; + for (std::uint64_t worker = 0; worker < Workers; ++worker) { + workers.emplace_back([integers, worker] { + for (std::uint64_t call = 0; call < Calls; ++call) Require(integers(worker, call, 1, 2, 3, 4, 5, 6) == Integers(worker, call, 1, 2, 3, 4, 5, 6)); + }); + } + for (auto& worker : workers) worker.join(); + constexpr std::size_t Window = 65536; + const auto recent = std::make_unique(Window); + Require(CopyCallTrace_nid_no_patch(recent.get(), Window) == Window); + Require(recent[Window - 1].sequence == 8 + Workers * Calls && recent[0].sequence == 8 + Workers * Calls - Window + 1); + for (std::size_t index = 0; index < Window; ++index) Require(std::strcmp(recent[index].name, "Integers") == 0); + Require(CopyCallCounts_nid_no_patch(counts, 1, &total) == 1 && total == 8 + Workers * Calls); + Require(counts[0].calls == 1 + Workers * Calls && std::strcmp(counts[0].name, "Integers") == 0); +} diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 60331fc87e..7601716b77 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI isupper_nid_postfix(int); int APS5_VABI islower_nid_postfix(int); @@ -20,10 +21,24 @@ int APS5_VABI toupper_nid_postfix(int); int APS5_VABI tolower_nid_postfix(int); const short* APS5_VABI _Getptolower_nid_postfix(); const short* APS5_VABI _Getptoupper_nid_postfix(); +const char* APS5_VABI setlocale_nid_postfix(int, const char*); } static void Require(bool value) { if (!value) std::abort(); } int main() { Require(std::setlocale(LC_CTYPE, "C") != nullptr); + for (int category = 0; category <= 6; ++category) { + const char* result = setlocale_nid_postfix(category, "C"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, "POSIX"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, nullptr); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, ""); + Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "en_US.UTF-8") == nullptr); + } + Require(setlocale_nid_postfix(-1, "C") == nullptr); + Require(setlocale_nid_postfix(7, "C") == nullptr); for (int c = EOF; c <= 255; ++c) { Require(bool(isupper_nid_postfix(c)) == bool(std::isupper(c))); Require(bool(islower_nid_postfix(c)) == bool(std::islower(c))); diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index ed4b1976d4..ddaf978819 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -127,21 +127,28 @@ int main() { const std::string longText = "collation of a longer text"; require(Vtable(facet).transform(&result, collate, longText.data(), longText.data() + longText.size()) == &result); - require(allocations == 2 && lastSize == longText.size() + 1 && result.pointer == lastAllocation); - require(result.size == longText.size() && result.capacity == longText.size() && std::string(result.pointer) == longText); + require(allocations == 2 && lastSize == 32 && result.pointer == lastAllocation); + require(result.size == longText.size() && result.capacity == 31 && std::string(result.pointer) == longText); std::free(result.pointer); - const std::string largest(4094, 'z'); - require(Vtable(facet).transform(&result, collate, largest.data(), largest.data() + largest.size()) == &result); - require(allocations == 3 && lastSize == 4095 && result.capacity == 4094 && std::string(result.pointer) == largest); + const std::string largestSmall(4079, 'z'); + require(Vtable(facet).transform(&result, collate, largestSmall.data(), largestSmall.data() + largestSmall.size()) == &result); + require(allocations == 3 && lastSize == 4080 && result.pointer == lastAllocation && result.capacity == 4079 && std::string(result.pointer) == largestSmall); std::free(result.pointer); - const std::string tooLong(4095, 'z'); - reject([&] { Vtable(facet).transform(&result, collate, tooLong.data(), tooLong.data() + tooLong.size()); }); + const std::string smallestLarge(4080, 'y'); + require(Vtable(facet).transform(&result, collate, smallestLarge.data(), smallestLarge.data() + smallestLarge.size()) == &result); + const auto raw = reinterpret_cast(lastAllocation); + const auto aligned = reinterpret_cast(result.pointer); + require(allocations == 4 && lastSize == 4096 + 0x27); + require(aligned % 0x20 == 0 && aligned >= raw + 8 && aligned <= raw + 0x27 && reinterpret_cast(result.pointer)[-1] == lastAllocation); + require(result.size == 4080 && result.capacity == 4095 && std::string(result.pointer) == smallestLarge); + std::free(lastAllocation); + const std::string embedded("a\0b", 3); reject([&] { Vtable(facet).transform(&result, collate, embedded.data(), embedded.data() + embedded.size()); }); reject([&] { Vtable(facet).transform(nullptr, collate, text, text + 1); }); - require(allocations == 3); + require(allocations == 4); Vtable(facet).facet.retain(facet); Vtable(facet).facet.retain(facet); diff --git a/core/libs/tests/GuestContentSearch.cpp b/core/libs/tests/GuestContentSearch.cpp new file mode 100644 index 0000000000..2447383112 --- /dev/null +++ b/core/libs/tests/GuestContentSearch.cpp @@ -0,0 +1,121 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param); +int APS5_VABI sceContentSearchTerm(void); +int APS5_VABI sceContentSearchGetContentLastUpdateId(std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchGetNumOfContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* numOfContent, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchGetTotalContentSize(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, std::int64_t* size, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchSearchContent(const ContentSearchContentColumnSet* columnSet, std::uint32_t columnSetLength, const ContentSearchContentOrderByCondition* orderByConditions, std::uint32_t orderByConditionsLength, std::uint32_t offset, std::uint32_t limit, std::int64_t* numOfContent, void* infos, std::int64_t* lastUpdateId); +int APS5_VABI sceContentSearchOpenMetadata(const char* filePath, std::int32_t* metadataId); +int APS5_VABI sceContentSearchOpenMetadataByContentId(std::int64_t contentId, std::int32_t* metadataId); +int APS5_VABI sceContentSearchCloseMetadata(std::int32_t metadataId); +int APS5_VABI sceContentSearchGetMetadataFieldInfo(std::int32_t metadataId, const char* field, std::int32_t* metadataType, std::int32_t* size); +int APS5_VABI sceContentSearchGetMetadataValue(std::int32_t metadataId, const char* field, void* value); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAction action) { + try { + action(); + } catch (const std::logic_error&) { + return true; + } + return false; +} + +constexpr int ErrorNotInitialized = static_cast(0x809D1001u); +constexpr int ErrorLimitTooBig = static_cast(0x809D100Au); + +struct MetadataValue { + std::int32_t size; + std::int32_t type; + std::int64_t value; +}; + +} + +int main() { + ContentSearchInitParam param{3 * 1024 * 1024}; + ContentSearchColumnValue photo{8, 1}; + ContentSearchContentColumn column{3, 0, &photo, 2}; + ContentSearchContentColumnSet columnSet{&column, 1, 2}; + ContentSearchContentOrderByCondition orderBy{4, 0}; + unsigned char infos[0x960 * 2] = {}; + std::int64_t count = -1; + std::int64_t size = -1; + std::int64_t updateId = -1; + std::int32_t metadataId = -1; + std::int32_t metadataType = -1; + std::int32_t fieldSize = -1; + MetadataValue value{-1, -1, -1}; + + Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == ErrorNotInitialized); + Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == ErrorNotInitialized); + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId) == ErrorNotInitialized); + Require(sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId) == ErrorNotInitialized); + Require(sceContentSearchOpenMetadataByContentId(1, &metadataId) == ErrorNotInitialized); + Require(sceContentSearchCloseMetadata(1) == ErrorNotInitialized); + Require(count == -1 && size == -1 && updateId == -1 && metadataId == -1); + + Require(sceContentSearchInit(¶m) == 0); + + Require(sceContentSearchGetContentLastUpdateId(&updateId) == 0); + Require(updateId == 0); + updateId = -1; + Require(sceContentSearchGetNumOfContent(&columnSet, 1, &count, &updateId) == 0); + Require(count == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchGetNumOfContent(nullptr, 0, &count, &updateId) == 0); + Require(count == 0 && updateId == 0); + updateId = -1; + Require(sceContentSearchGetTotalContentSize(&columnSet, 1, &size, &updateId) == 0); + Require(size == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 92, &count, infos, &updateId) == 0); + Require(count == 0 && updateId == 0); + count = -1; + updateId = -1; + Require(sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 93, &count, infos, &updateId) == ErrorLimitTooBig); + Require(count == -1 && updateId == -1); + + Require(Throws([&] { sceContentSearchGetContentLastUpdateId(nullptr); })); + Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, nullptr, &updateId); })); + Require(Throws([&] { sceContentSearchGetNumOfContent(&columnSet, 1, &count, nullptr); })); + Require(Throws([&] { sceContentSearchGetTotalContentSize(nullptr, 1, &size, &updateId); })); + Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &orderBy, 1, 0, 2, &count, nullptr, &updateId); })); + ContentSearchContentColumn unknownColumn{8, 0, &photo, 2}; + ContentSearchContentColumnSet unknownColumnSet{&unknownColumn, 1, 2}; + Require(Throws([&] { sceContentSearchGetNumOfContent(&unknownColumnSet, 1, &count, &updateId); })); + ContentSearchContentColumn missingValue{3, 0, nullptr, 2}; + ContentSearchContentColumnSet missingValueSet{&missingValue, 1, 2}; + Require(Throws([&] { sceContentSearchGetTotalContentSize(&missingValueSet, 1, &size, &updateId); })); + ContentSearchContentColumn unknownOperator{3, 6, &photo, 2}; + ContentSearchContentColumnSet unknownOperatorSet{&unknownOperator, 1, 2}; + Require(Throws([&] { sceContentSearchSearchContent(&unknownOperatorSet, 1, &orderBy, 1, 0, 2, &count, infos, &updateId); })); + ContentSearchContentOrderByCondition unknownSort{3, 0}; + Require(Throws([&] { sceContentSearchSearchContent(&columnSet, 1, &unknownSort, 1, 0, 2, &count, infos, &updateId); })); + Require(count == -1 && updateId == -1); + + Require(Throws([&] { sceContentSearchOpenMetadata("/data/photo.jpg", &metadataId); })); + Require(Throws([&] { sceContentSearchOpenMetadataByContentId(1, &metadataId); })); + Require(metadataId == -1); + Require(Throws([] { sceContentSearchCloseMetadata(1); })); + Require(Throws([&] { sceContentSearchGetMetadataFieldInfo(1, "title", &metadataType, &fieldSize); })); + Require(Throws([&] { sceContentSearchGetMetadataValue(1, "title", &value); })); + Require(metadataType == -1 && fieldSize == -1 && value.size == -1); + + Require(sceContentSearchTerm() == 0); + Require(sceContentSearchGetContentLastUpdateId(&updateId) == ErrorNotInitialized); + Require(updateId == -1); +} diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp index 5499463006..a70891bf8a 100644 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ b/core/libs/tests/GuestContentUnimplemented.cpp @@ -6,10 +6,7 @@ extern "C" { int APS5_VABI sceContentDeleteById(); -int APS5_VABI sceContentSearchGetContentLastUpdateId(); -int APS5_VABI sceContentSearchOpenMetadataByContentId(); -int APS5_VABI sceContentSearchGetTotalContentSize(); -int APS5_VABI sceContentSearchGetNumOfContent(); + } namespace { @@ -31,9 +28,6 @@ bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { int main() { bool correct = true; correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - correct &= ReportsNotImplemented("sceContentSearchGetContentLastUpdateId", sceContentSearchGetContentLastUpdateId); - correct &= ReportsNotImplemented("sceContentSearchOpenMetadataByContentId", sceContentSearchOpenMetadataByContentId); - correct &= ReportsNotImplemented("sceContentSearchGetTotalContentSize", sceContentSearchGetTotalContentSize); - correct &= ReportsNotImplemented("sceContentSearchGetNumOfContent", sceContentSearchGetNumOfContent); + return correct ? 0 : 1; } diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp new file mode 100644 index 0000000000..5c12d2cddc --- /dev/null +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); +int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); +int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); +int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size); +} + +namespace { +constexpr int NotInCoredumpHandler = static_cast(0x81180003u); +constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; +bool handlerRan = false; +int handlerContext = 0; + +int APS5_VABI Handler(void*) { + handlerRan = true; + return 0; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAttach attach, const char* function) { + try { attach(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), function) != nullptr; } + return false; +} +} + +int main() { + alignas(uint64_t) std::array region{}; + for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 7 + 3); + const auto original = region; + + Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(UserFileTruncateIfExceedLimit | 2, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + Require(region == original); + + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); + Require(sceCoredumpAttachMemoryRegion(0, region.data(), 1) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(0xFFFFFFFFu, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + int fromThread = 0; + std::thread([&] { fromThread = sceCoredumpAttachMemoryRegion(3, region.data(), region.size()); }).join(); + Require(fromThread == NotInCoredumpHandler); + std::thread([&] { fromThread = sceCoredumpGetStopInfoCpu(region.data() + 16, 16); }).join(); + Require(fromThread == NotInCoredumpHandler); + Require(region == original); + Require(!handlerRan); + + Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, nullptr, region.size()); }, "sceCoredumpAttachMemoryRegion")); + Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), 0); }, "sceCoredumpAttachMemoryRegion")); + Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachMemoryRegion")); + Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, nullptr, region.size()); }, "sceCoredumpAttachUserMemoryFile")); + Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), 0); }, "sceCoredumpAttachUserMemoryFile")); + Require(Throws([&] { sceCoredumpAttachUserMemoryFile(1, region.data(), SIZE_MAX); }, "sceCoredumpAttachUserMemoryFile")); + Require(Throws([&] { sceCoredumpGetStopInfoCpu(nullptr, 16); }, "sceCoredumpGetStopInfoCpu")); + Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 1, 16); }, "sceCoredumpGetStopInfoCpu")); + Require(Throws([&] { sceCoredumpGetStopInfoCpu(reinterpret_cast(UINTPTR_MAX - 7u), 16); }, "sceCoredumpGetStopInfoCpu")); + for (const size_t size : {size_t{0}, size_t{1}, size_t{15}, size_t{17}, SIZE_MAX}) + Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 16, size); }, "sceCoredumpGetStopInfoCpu")); + + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpAttachUserMemoryFile(1, region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); + Require(region == original); + Require(!handlerRan); + return 0; +} diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp new file mode 100644 index 0000000000..30776ebbcc --- /dev/null +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); +int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); +int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path); +int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name); +} + +namespace { +constexpr int64_t NotInCoredumpHandler = static_cast(0x81180003u); +bool handlerRan = false; +int handlerContext = 0; + +int APS5_VABI Handler(void*) { + handlerRan = true; + return 0; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TCall call, const char* function) { + try { call(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), function) != nullptr; } + return false; +} + +void RequireWritesRefused(const std::array& region) { + Require(sceCoredumpWriteUserData(region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), 1) == NotInCoredumpHandler); + int64_t fromThread = 0; + std::thread([&] { fromThread = sceCoredumpWriteUserData(region.data(), region.size()); }).join(); + Require(fromThread == NotInCoredumpHandler); +} + +void RequireAttachesRefused(const std::array& region) { + Require(sceCoredumpAttachUserFile(1, "/app0/eboot.bin") == NotInCoredumpHandler); + Require(sceCoredumpAttachUserFile(0xFFFFFFFFu, "") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(999, region.data(), region.size(), "region.txt") == NotInCoredumpHandler); + Require(sceCoredumpAttachMemoryRegionAsUserFile(0, region.data(), 1, "") == NotInCoredumpHandler); + int fromThread = 0; + std::thread([&] { fromThread = sceCoredumpAttachMemoryRegionAsUserFile(2, region.data(), region.size(), "thread.txt"); }).join(); + Require(fromThread == NotInCoredumpHandler); +} +} + +int main() { + std::array region{}; + for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 5 + 1); + const auto original = region; + const char text[] = "coredump text\n"; + + Require(NotInCoredumpHandler < 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + sceCoredumpDebugTextOut(text, 8); + sceCoredumpDebugTextOut(text, 0); + sceCoredumpDebugTextOut(nullptr, 0); + + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + Require(!handlerRan); + + Require(Throws([&] { sceCoredumpWriteUserData(nullptr, region.size()); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), 0); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), SIZE_MAX); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); + Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); + Require(Throws([&] { sceCoredumpAttachUserFile(1, nullptr); }, "sceCoredumpAttachUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, nullptr, region.size(), "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), 0, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), SIZE_MAX, "a.txt"); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + Require(Throws([&] { sceCoredumpAttachMemoryRegionAsUserFile(1, region.data(), region.size(), nullptr); }, "sceCoredumpAttachMemoryRegionAsUserFile")); + + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + RequireWritesRefused(region); + RequireAttachesRefused(region); + Require(region == original); + Require(!handlerRan); + return 0; +} diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index 99e0035288..01cc2d3003 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -9,6 +9,7 @@ int APS5_VABI seteuid_nid_postfix(std::uint32_t); int APS5_VABI setegid_nid_postfix(std::uint32_t); int APS5_VABI setuid_nid_postfix(std::uint32_t); int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setregid_nid_postfix(std::uint32_t, std::uint32_t); int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); int APS5_VABI getuid_nid_postfix(void); @@ -19,6 +20,7 @@ int* APS5_VABI __error_nid_postfix(); static void Require(bool value) { if (!value) std::abort(); } using SetId = int (APS5_VABI *)(std::uint32_t); +using SetReId = int (APS5_VABI *)(std::uint32_t, std::uint32_t); static bool Throws(SetId function, std::uint32_t id) { try { @@ -29,6 +31,15 @@ static bool Throws(SetId function, std::uint32_t id) { return false; } +static bool ThrowsRe(SetReId function, std::uint32_t r, std::uint32_t e) { + try { + function(r, e); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr std::uint32_t Unchanged = 0xffffffffu; @@ -54,6 +65,15 @@ int main() { Require(setreuid_nid_postfix(0, Unchanged) == 0); Require(setreuid_nid_postfix(Unchanged, 0) == 0); Require(setreuid_nid_postfix(0, 0) == 0); + Require(ThrowsRe(setreuid_nid_postfix, 1000, 0)); + Require(ThrowsRe(setreuid_nid_postfix, 0, 1000)); + + Require(setregid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setregid_nid_postfix(0, Unchanged) == 0); + Require(setregid_nid_postfix(Unchanged, 0) == 0); + Require(setregid_nid_postfix(0, 0) == 0); + Require(ThrowsRe(setregid_nid_postfix, 1000, 0)); + Require(ThrowsRe(setregid_nid_postfix, 0, 1000)); std::array groups{}; Require(getgroups_nid_postfix(0, nullptr) == 1); diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index d628a0e72e..8e358e7c86 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -6,10 +6,12 @@ #include #include #include +#include #include extern "C" { void* APS5_VABI opendir_nid_postfix(const char*); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); +int APS5_VABI readdir_r_nid_postfix(void*, GuestDirectoryEntry*, GuestDirectoryEntry**); int APS5_VABI closedir_nid_postfix(void*); void APS5_VABI rewinddir_nid_postfix(void*); int* APS5_VABI __error_nid_postfix(); @@ -39,6 +41,50 @@ int main() { std::size_t count = 0; while (readdir_nid_postfix(directory)) ++count; Require(count == entries.size()); + rewinddir_nid_postfix(directory); + std::map copies; + GuestDirectoryEntry buffer; + GuestDirectoryEntry* result = nullptr; + *__error_nid_postfix() = 13; + for (;;) { + std::memset(&buffer, 0x7f, sizeof(buffer)); + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0); + if (!result) break; + Require(result == &buffer); + Require(buffer.nameLength == std::strlen(buffer.name)); + Require(buffer.recordLength == 8 + ((buffer.nameLength + 4) & ~3)); + for (std::size_t i = buffer.recordLength; i < sizeof(buffer); ++i) + Require(reinterpret_cast(&buffer)[i] == 0x7f); + copies[buffer.name] = buffer.type; + } + Require(copies == entries); + Require(*__error_nid_postfix() == 13); + result = &buffer; + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0 && result == nullptr); + rewinddir_nid_postfix(directory); + *__error_nid_postfix() = 13; + auto* first = readdir_nid_postfix(directory); + Require(first != nullptr); + const std::string firstName = first->name; + Require(readdir_r_nid_postfix(directory, &buffer, &result) == 0 && result == &buffer); + Require(firstName != buffer.name && entries.count(buffer.name) == 1); + result = &buffer; + Require(readdir_r_nid_postfix(nullptr, &buffer, &result) == 9); + Require(result == &buffer && *__error_nid_postfix() == 13); + bool nullEntryThrows = false; + try { + readdir_r_nid_postfix(directory, nullptr, &result); + } catch (const std::invalid_argument&) { + nullEntryThrows = true; + } + Require(nullEntryThrows); + bool nullResultThrows = false; + try { + readdir_r_nid_postfix(directory, &buffer, nullptr); + } catch (const std::invalid_argument&) { + nullResultThrows = true; + } + Require(nullResultThrows); Require(closedir_nid_postfix(directory) == 0); Require(opendir_nid_postfix((root / "missing").string().c_str()) == nullptr); Require(*__error_nid_postfix() == 2); diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index 6d1e6f4c30..bfd1beb0dd 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include extern "C" { @@ -17,9 +18,6 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*); -int APS5_VABI getdents_nid_postfix(int, char*, int); -int APS5_VABI getdirentries_nid_postfix(int, char*, int, std::int64_t*); -int* APS5_VABI __error_nid_postfix(); } static void Check(bool value, int line) { @@ -82,24 +80,13 @@ static void CheckOversizedName() { const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); Require(directory >= 0); std::array buffer; - buffer.fill('x'); - Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == 24); - Require(std::strcmp(buffer.data() + 8, ".") == 0); - Require(std::strcmp(buffer.data() + 20, "..") == 0); - Require(std::all_of(buffer.begin() + 24, buffer.end(), [](char byte) { return byte == 'x'; })); - for (int attempt = 0; attempt < 2; ++attempt) { - buffer.fill('x'); - Require(sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())) == SCE_KERNEL_ERROR_ENAMETOOLONG); - Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + bool threw = false; + try { + sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + } catch (const std::runtime_error&) { + threw = true; } - *__error_nid_postfix() = 0; - Require(getdents_nid_postfix(directory, buffer.data(), static_cast(buffer.size())) == -1); - Require(*__error_nid_postfix() == 63); - *__error_nid_postfix() = 0; - std::int64_t base = -1; - Require(getdirentries_nid_postfix(directory, buffer.data(), static_cast(buffer.size()), &base) == -1); - Require(*__error_nid_postfix() == 63); - Require(std::all_of(buffer.begin(), buffer.end(), [](char byte) { return byte == 'x'; })); + Require(threw); Require(sceKernelClose(directory) == 0); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index ee576b3c01..a7cb911bef 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); } static void Check(bool value, int line) { @@ -104,6 +106,23 @@ int main() { Require(sceKernelLseek(directory, 0, 0) == 0); Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c", "d"})); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const int duplicate = dup_nid_postfix(directory); + Require(duplicate >= 0 && duplicate != directory); + Require(sceKernelLseek(duplicate, 0, 1) == 2); + Require(ReadAll(duplicate) == (std::set{"a", "b", "c", "d"})); + Require(sceKernelLseek(directory, 0, 1) == sceKernelLseek(duplicate, 0, 1)); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelLseek(duplicate, 0, 1) == 0); + const int target = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(target >= 0); + Require(dup2_nid_postfix(duplicate, target) == target); + Require(sceKernelClose(duplicate) == 0); + Require(ReadAll(target) == (std::set{".", "..", "a", "b", "c", "d"})); + Require(sceKernelLseek(directory, 0, 1) == sceKernelLseek(target, 0, 1)); + Require(sceKernelClose(target) == 0); + Require(sceKernelClose(directory) == 0); const int reused = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDWR, 0); diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index e588f786da..a3ebfccef8 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -11,6 +11,10 @@ int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; Require(std::filesystem::is_directory("download0") == (mode == "declared")); + const bool data = std::filesystem::is_directory("data"); + std::filesystem::remove_all("data"); + Require(data == (mode != "missing")); + if (mode == "missing") return 0; if (mode == "invalid") { bool rejected = false; try { diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index e4241dc522..04614f8062 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -24,7 +24,8 @@ static void Require(bool value) { if (!value) std::abort(); } constexpr int GuestEbadf = 9; constexpr int Unix = 1; constexpr int Stream = 1; -constexpr int FirstSocket = 0x10000000; +constexpr int DescriptorSetSize = 1024; +constexpr int Unopened = 0x1ffffff0; static void RequireFailure(int result, int error) { Require(result == -1); @@ -60,7 +61,7 @@ static void DupOutlivesOriginal() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); const int duplicate = dup_nid_postfix(pipe[1]); - Require(duplicate >= 0 && duplicate < FirstSocket && duplicate != pipe[1]); + Require(duplicate >= 0 && duplicate != pipe[1]); Require(close_nid_postfix(pipe[1]) == 0); RequirePipeCarries(duplicate, pipe[0], "dup"); Require(close_nid_postfix(duplicate) == 0); @@ -97,8 +98,8 @@ static void BadDescriptors() { RequireFailure(dup2_nid_postfix(closed, closed), GuestEbadf); RequireFailure(dup2_nid_postfix(pipe[0], -1), GuestEbadf); RequireFailure(dup2_nid_postfix(-1, pipe[0]), GuestEbadf); - RequireFailure(dup_nid_postfix(FirstSocket + 0x0ffffff0), GuestEbadf); - RequireFailure(dup2_nid_postfix(FirstSocket + 0x0ffffff0, FirstSocket + 0x0ffffff1), GuestEbadf); + RequireFailure(dup_nid_postfix(Unopened), GuestEbadf); + RequireFailure(dup2_nid_postfix(Unopened, Unopened + 1), GuestEbadf); #ifdef _WIN32 const auto previous = _set_invalid_parameter_handler(RecordInvalidParameter); #endif @@ -115,25 +116,29 @@ static void SocketDuplicates() { int pair[2]; Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); const int duplicate = dup_nid_postfix(pair[0]); - Require(duplicate >= FirstSocket && duplicate != pair[0] && duplicate != pair[1]); + Require(duplicate >= 0 && duplicate < DescriptorSetSize && duplicate != pair[0] && duplicate != pair[1]); Require(close_nid_postfix(pair[0]) == 0); RequireSocketCarries(duplicate, pair[1], "socket"); - const int target = duplicate + 0x1000; + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const int target = pipe[0]; Require(dup2_nid_postfix(duplicate, target) == target); Require(close_nid_postfix(duplicate) == 0); RequireSocketCarries(target, pair[1], "moved"); Require(dup2_nid_postfix(target, target) == target); int next[2]; Require(socketpair_nid_postfix(Unix, Stream, 0, next) == 0); - Require(next[0] > target && next[1] > target); + Require(next[0] < DescriptorSetSize && next[1] < DescriptorSetSize); RequireSocketCarries(target, pair[1], "kept"); Require(dup2_nid_postfix(next[0], target) == target); RequireSocketCarries(target, next[1], "over"); Require(Throws([&] { dup2_nid_postfix(target, 1); })); - int pipe[2]; - Require(pipe_nid_postfix(pipe) == 0); - Require(Throws([&] { dup2_nid_postfix(pipe[0], target); })); - for (const int descriptor : {target, pair[1], next[0], next[1], pipe[0], pipe[1]}) Require(close_nid_postfix(descriptor) == 0); + int other[2]; + Require(pipe_nid_postfix(other) == 0); + Require(dup2_nid_postfix(other[1], target) == target); + RequirePipeCarries(target, other[0], "file"); + Require(send_nid_postfix(target, "x", 1, 0) == -1); + for (const int descriptor : {target, pair[1], next[0], next[1], pipe[1], other[0], other[1]}) Require(close_nid_postfix(descriptor) == 0); } static void RandomDeviceDuplicates() { @@ -171,7 +176,7 @@ static void RandomDeviceDuplicates() { Require(read_nid_postfix(secondRandom, randomBuf4, sizeof(randomBuf4)) == sizeof(randomBuf4)); Require(std::memcmp(randomBuf4, randomBuf3, sizeof(randomBuf4)) != 0); - const int invalidFd = FirstSocket - 100; + const int invalidFd = Unopened - 100; RequireFailure(dup2_nid_postfix(invalidFd, secondRandom), GuestEbadf); unsigned char randomBuf5[16] = {}; Require(read_nid_postfix(secondRandom, randomBuf5, sizeof(randomBuf5)) == sizeof(randomBuf5)); diff --git a/core/libs/tests/GuestDynamicCast.cpp b/core/libs/tests/GuestDynamicCast.cpp new file mode 100644 index 0000000000..c7892a5e96 --- /dev/null +++ b/core/libs/tests/GuestDynamicCast.cpp @@ -0,0 +1,45 @@ +#include "prx/libc/include/exceptions/Runtime.hpp" +#include +#include +#include +#include + +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct Vtable { std::intptr_t offsetToTop; const TypeRecord* type; const void* first; }; +struct Object { const void* vtable; }; + +extern "C" { +extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; +extern const unsigned char _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; +void* APS5_VABI __dynamic_cast_nid_postfix(const void*, const void*, const void*, std::ptrdiff_t); +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void*, std::type_info*, void (*)(void*)); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void Destroy(void*) {} + +const TypeRecord BaseType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "4Base", nullptr}; +const TypeRecord DerivedType{_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 16, "7Derived", &BaseType}; +const TypeRecord OtherType{_ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix + 16, "5Other", &BaseType}; +const Vtable BaseVtable{0, &BaseType, nullptr}; +const Vtable DerivedVtable{0, &DerivedType, nullptr}; + +} // namespace + +int main() { + const Object derived{&DerivedVtable.first}; + const Object base{&BaseVtable.first}; + Require(__dynamic_cast_nid_postfix(&derived, &BaseType, &DerivedType, 0) == &derived); + Require(__dynamic_cast_nid_postfix(&derived, &BaseType, &OtherType, 0) == nullptr); + Require(__dynamic_cast_nid_postfix(&base, &BaseType, &DerivedType, 0) == nullptr); + Require(__dynamic_cast_nid_postfix(nullptr, &BaseType, &DerivedType, 0) == nullptr); + void* object = __cxa_allocate_exception_nid_postfix(16); + auto* type = reinterpret_cast(const_cast(&DerivedType)); + LibcException::Header* header = __cxa_init_primary_exception_nid_postfix(object, type, Destroy); + Require(header == LibcException::FromObject(object)); + Require(header->type == type && header->destructor == Destroy && header->adjusted == object); + __cxa_free_exception_nid_postfix(object); +} diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 56819452f3..8e97329913 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -20,6 +20,7 @@ int APS5_VABI sceKernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTime int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata); int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id); intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev); +int APS5_VABI sceKernelGetEventError(const KernelEvent* ev); intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev); int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev); @@ -34,6 +35,7 @@ static constexpr int EVFILT_TIMER = -7; static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; static constexpr int EVFILT_AMPR = -25; +static constexpr std::uint16_t EV_ERROR = 0x4000; static void Require(bool value) { if (!value) std::abort(); } @@ -77,6 +79,7 @@ static void VerifyPeriodicTimer() { Require(sceKernelGetEventFilter(&events[0]) == EVFILT_TIMER); Require(sceKernelGetEventData(&events[0]) >= 5); Require(sceKernelGetEventUserData(&events[0]) == &first); + Require(sceKernelGetEventError(&events[0]) == 0); Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); @@ -288,8 +291,15 @@ int main() { Require(sceKernelGetEventFflags(&rawEvent) == static_cast(0x80000001LL)); Require(sceKernelGetEventData(&rawEvent) == -5); Require(sceKernelGetEventUserData(&rawEvent) == nullptr); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.flags = static_cast(~EV_ERROR); + Require(sceKernelGetEventError(&rawEvent) == 0); + rawEvent.flags = EV_ERROR; + rawEvent.data = 22; + Require(sceKernelGetEventError(&rawEvent) == 22); Require(RejectsNull(sceKernelGetEventData)); + Require(RejectsNull(sceKernelGetEventError)); Require(RejectsNull(sceKernelGetEventFflags)); Require(RejectsNull(sceKernelGetEventFilter)); Require(RejectsNull(sceKernelGetEventId)); diff --git a/core/libs/tests/GuestExecvp.cpp b/core/libs/tests/GuestExecvp.cpp index ebf28b63a8..24044df2f7 100644 --- a/core/libs/tests/GuestExecvp.cpp +++ b/core/libs/tests/GuestExecvp.cpp @@ -1,23 +1,29 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" #include #include #include -extern "C" int APS5_VABI execvp_nid_postfix(const char*, char* const*); -static void Require(bool value) { if (!value) std::abort(); } -static bool ReportsNotImplemented(const char* file, char* const* arguments) { +extern "C" { +int APS5_VABI execvp_nid_postfix(const char*, char* const*); +int APS5_VABI execv_nid_postfix(const char*, char* const*); +int APS5_VABI execve_nid_postfix(const char*, char* const*, char* const*); +} +template static bool Throws(TCall call, const std::string& message) { try { - execvp_nid_postfix(file, arguments); + call(); } catch (const std::runtime_error& error) { - return std::string(error.what()) == "execvp: executable replacement not implemented"; + return error.what() == message; } return false; } +static void Require(bool value) { if (!value) std::abort(); } int main() { - char shell[] = "sh"; - char option[] = "-c"; - char command[] = "exit 0"; - char* arguments[] = {shell, option, command, nullptr}; - Require(ReportsNotImplemented("sh", arguments)); - Require(ReportsNotImplemented("/bin/sh", arguments)); - Require(ReportsNotImplemented(nullptr, nullptr)); + char name[] = "sh"; + char* arguments[] = {name, nullptr}; + char variable[] = "HOME=/"; + char* environment[] = {variable, nullptr}; + Require(Throws([&] { execvp_nid_postfix("sh", arguments); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execvp_nid_postfix("/bin/sh", arguments); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execvp_nid_postfix(nullptr, nullptr); }, "execvp: executable replacement not implemented")); + Require(Throws([&] { execv_nid_postfix("/bin/sh", arguments); }, "execv: executable replacement not implemented")); + Require(Throws([&] { execve_nid_postfix("/bin/sh", arguments, environment); }, "execve: executable replacement not implemented")); } diff --git a/core/libs/tests/GuestFdopendir.cpp b/core/libs/tests/GuestFdopendir.cpp new file mode 100644 index 0000000000..4e6cfffa82 --- /dev/null +++ b/core/libs/tests/GuestFdopendir.cpp @@ -0,0 +1,49 @@ +#include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include +#include +#include +#include +#include +#include +#include +extern "C" { +void* APS5_VABI fdopendir_nid_postfix(int); +GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); +int APS5_VABI closedir_nid_postfix(void*); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +int main() { + const auto root = std::filesystem::path("anyps5-fdopendir-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + Require(std::filesystem::create_directory(root / "subdirectory")); + { std::ofstream file(root / "sample.txt"); file << "test"; } + const int fd = open_nid_postfix(root.string().c_str(), 0x20000, 0); + Require(fd >= 0); + void* directory = fdopendir_nid_postfix(fd); + Require(directory != nullptr); + std::map entries; + while (auto* entry = readdir_nid_postfix(directory)) { + entries[entry->name] = entry->type; + } + Require(entries.at("sample.txt") == 8); + Require(entries.at("subdirectory") == 4); + Require(DescriptorIsOpen_nid_no_patch(fd)); + Require(closedir_nid_postfix(directory) == 0); + Require(!DescriptorIsOpen_nid_no_patch(fd)); + Require(fdopendir_nid_postfix(fd) == nullptr && *__error_nid_postfix() == 9); + Require(fdopendir_nid_postfix(-1) == nullptr && *__error_nid_postfix() == 9); + const int file = open_nid_postfix((root / "sample.txt").string().c_str(), 0, 0); + Require(file >= 0); + Require(fdopendir_nid_postfix(file) == nullptr && *__error_nid_postfix() == 20); + Require(DescriptorIsOpen_nid_no_patch(file)); + Require(close_nid_postfix(file) == 0); + std::filesystem::remove(root / "sample.txt"); + std::filesystem::remove(root / "subdirectory"); + std::filesystem::remove(root); +} diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 48a1758954..13c71a958d 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -12,9 +12,11 @@ extern "C" { int APS5_VABI utimes_nid_postfix(const char*, const KernelTimeval*); +int APS5_VABI lutimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI stat_nid_postfix(const char*, FileStat*); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); @@ -72,6 +74,8 @@ int main() { CheckTimes(path, fd, second); Require(sceKernelUtimes_nid_postfix(path.c_str(), first) == 0, "sceKernelUtimes fractional timestamp"); CheckTimes(path, fd, first); + Require(lutimes_nid_postfix(path.c_str(), second) == 0, "lutimes fractional timestamp"); + CheckTimes(path, fd, second); for (int index = 0; index < 2; ++index) { for (const auto micros : {-1LL, 1000000LL}) { @@ -80,7 +84,8 @@ int main() { Require(utimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "utimes rejects invalid microseconds"); Require(futimes_nid_postfix(fd, invalid) == -1 && *__error_nid_postfix() == 22, "futimes rejects invalid microseconds"); Require(sceKernelUtimes_nid_postfix(path.c_str(), invalid) == static_cast(0x80020016u), "sceKernelUtimes rejects invalid microseconds"); - CheckTimes(path, fd, first); + Require(lutimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "lutimes rejects invalid microseconds"); + CheckTimes(path, fd, second); } } @@ -137,6 +142,35 @@ int main() { Require(close_nid_postfix(directoryFd) == 0, "close directory descriptor"); Require(utimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "missing path reports ENOENT"); + Require(lutimes_nid_postfix(nullptr, first) == -1 && *__error_nid_postfix() == 14, "lutimes rejects null path"); + Require(lutimes_nid_postfix("", first) == -1 && *__error_nid_postfix() == 2, "lutimes rejects empty path"); + Require(lutimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "lutimes missing path reports ENOENT"); + Require(utimes_nid_postfix(path.c_str(), second) == 0, "utimes target to second"); + std::error_code symlinkError; + const auto linkPath = (root / "link.txt").string(); + std::filesystem::create_symlink(path, linkPath, symlinkError); + if (!symlinkError) { + Require(lutimes_nid_postfix(linkPath.c_str(), first) == 0, "lutimes modifies symlink"); + FileStat targetStatus{}; + Require(stat_nid_postfix(path.c_str(), &targetStatus) == 0 && Same(targetStatus.st_mtim, second[1]), "target file unchanged"); +#ifdef _WIN32 + const HANDLE handle = CreateFileW(std::filesystem::path(linkPath).c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "open symlink"); + FILETIME modified{}; + Require(GetFileTime(handle, nullptr, nullptr, &modified), "get symlink time"); + CloseHandle(handle); + const auto ticks = [](FILETIME time) { + return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; + }; + Require(ticks(modified) == static_cast(first[1].tv_sec + 11644473600LL) * 10000000 + first[1].tv_usec * 10, + "symlink mtime updated"); +#else + FileStat linkStatus{}; + Require(lstat_nid_postfix(linkPath.c_str(), &linkStatus) == 0 && Same(linkStatus.st_mtim, first[1]), "symlink mtime updated"); +#endif + } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0, "create pipe"); FileStat pipeStatus{}; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index d32843235b..188f1595fe 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); @@ -68,6 +69,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*) int* APS5_VABI __error_nid_postfix(); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); int APS5_VABI sceKernelDebugOutText(int, const char*); @@ -92,6 +94,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())); @@ -174,8 +185,36 @@ static void TestParentTraversal() { std::filesystem::remove_all(root); } +static void TestRelinkedModuleNames() { + const auto root = std::filesystem::path("anyps5-relinked-names-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + const auto original = (root / "Plugin.prx").generic_string(); + const auto relinked = original + ".guest.prx"; + FileStat status{}; + Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); + Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); + Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); + { std::ofstream file(relinked); file << "relinked"; } + Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); + Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); + Require(sceKernelCheckReachability(original.c_str()) == 0); + const int opened = sceKernelOpen(original.c_str(), 0, 0); + Require(opened >= 0 && sceKernelClose(opened) == 0); + Require(sceKernelStat((root / "Other.prx").generic_string().c_str(), &status) == static_cast(0x80020002u)); + Require(!std::filesystem::exists(original)); + { std::ofstream file(original); file << "original-bytes"; } + Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 14); + Require(std::filesystem::remove(original)); + const int created = sceKernelOpen(original.c_str(), 0x601, 0600); + Require(created >= 0 && sceKernelClose(created) == 0); + Require(std::filesystem::file_size(original) == 0 && std::filesystem::file_size(relinked) == 8); + std::filesystem::remove_all(root); +} + int main() { TestParentTraversal(); + TestRelinkedModuleNames(); 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); @@ -217,6 +256,20 @@ int main() { Require(read_nid_postfix(descriptors[0], received, sizeof(received)) == sizeof(received)); Require(std::memcmp(received, payload, sizeof(payload)) == 0); Require(read_nid_postfix(descriptors[0], received, sizeof(received)) == 0); + { + int unseekable[2] = {-1, -1}; + Require(pipe_nid_postfix(unseekable) == 0 && write_nid_postfix(unseekable[1], "pipe", 4) == 4); + for (const int end : unseekable) { + for (int whence = 0; whence < 3; ++whence) { + Require(lseek_nid_postfix(end, 0, whence) == -1 && *__error_nid_postfix() == 29); + Require(sceKernelLseek(end, 1, whence) == static_cast(0x8002001du)); + } + } + char piped[8]{}; + Require(read_nid_postfix(unseekable[0], piped, sizeof(piped)) == 4 && std::memcmp(piped, "pipe", 4) == 0); + Require(close_nid_postfix(unseekable[0]) == 0 && close_nid_postfix(unseekable[1]) == 0); + Require(lseek_nid_postfix(unseekable[0], 0, 1) == -1 && *__error_nid_postfix() == 9); + } { std::filesystem::remove("creat_probe.bin"); int created = creat_nid_postfix("creat_probe.bin", 0644); @@ -250,6 +303,34 @@ int main() { std::rewind(hostCapture); Require(std::fread(debugText, 1, sizeof(debugText), hostCapture) == 15 && std::memcmp(debugText, "[debug:1] kept\n", 15) == 0); Require(std::fclose(hostCapture) == 0); +#ifdef _WIN32 + for (const bool reopen : {false, true}) { + auto* hostCapture = std::tmpfile(); + const int hostError = ::_dup(2); + Require(hostCapture != nullptr && hostError >= 0); + int redirect[2] = {-1, -1}; + Require(reopen || pipe_nid_postfix(redirect) == 0); + const bool captured = ::_dup2(::_fileno(hostCapture), 2) == 0 && SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast(::_get_osfhandle(2))); + const bool redirected = reopen ? freopen_nid_postfix("stderr_probe.txt", "w", &_Stderr_nid_postfix) == &_Stderr_nid_postfix : dup2_nid_postfix(redirect[1], 2) == 2; + const HANDLE kept = GetStdHandle(STD_ERROR_HANDLE); + DWORD count = 0; + const bool separate = kept != reinterpret_cast(::_get_osfhandle(2)) && WriteFile(kept, "kept", 4, &count, nullptr) && count == 4; + const bool written = write_nid_postfix(2, "G", 1) == 1; + const bool restored = ::_dup2(hostError, 2) == 0 && ::_close(hostError) == 0 && SetStdHandle(STD_ERROR_HANDLE, reinterpret_cast(::_get_osfhandle(2))); + Require(captured && redirected && separate && written && restored && CloseHandle(kept)); + std::rewind(hostCapture); + char keptText[8]{}; + Require(std::fread(keptText, 1, sizeof(keptText), hostCapture) == 4 && std::memcmp(keptText, "kept", 4) == 0); + Require(std::fclose(hostCapture) == 0); + char guestText = 0; + if (reopen) { + Require(std::filesystem::file_size("stderr_probe.txt") == 1 && std::filesystem::remove("stderr_probe.txt")); + } else { + Require(close_nid_postfix(redirect[1]) == 0 && read_nid_postfix(redirect[0], &guestText, 1) == 1 && guestText == 'G'); + Require(close_nid_postfix(redirect[0]) == 0); + } + } +#endif const auto root = std::filesystem::path("anyps5-filesystem-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(mkdir_nid_postfix(root.string().c_str(), 0700) == 0); @@ -258,6 +339,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"; } @@ -328,6 +423,23 @@ int main() { Require(rename_nid_postfix(name(targetFile).c_str(), name(targetFile).c_str()) == 0); Require(rename_nid_postfix(name(sourceDirectory).c_str(), name(emptyDirectory).c_str()) == 0); Require(!std::filesystem::exists(sourceDirectory) && std::filesystem::is_directory(emptyDirectory / "child")); + for (const bool kernel : {false, true}) { + const auto incoming = name(area / "incoming.txt"); + const auto occupied = name(area / "occupied.txt"); + const auto replace = [&] { return kernel ? sceKernelRename(incoming.c_str(), occupied.c_str()) : rename_nid_postfix(incoming.c_str(), occupied.c_str()); }; + const auto contents = [&] { std::ifstream stream(occupied); std::string text; std::getline(stream, text); return text; }; + { std::ofstream stream(incoming); stream << "incoming"; } + { std::ofstream stream(occupied); stream << "occupied"; } + const int occupiedDescriptor = sceKernelOpen(occupied.c_str(), 2, 0); + Require(occupiedDescriptor >= 0 && replace() == 0 && !std::filesystem::exists(incoming) && contents() == "incoming"); + char occupiedBytes[8]{}; + Require(read_nid_postfix(occupiedDescriptor, occupiedBytes, sizeof(occupiedBytes)) == sizeof(occupiedBytes) && std::memcmp(occupiedBytes, "occupied", 8) == 0); + Require(sceKernelClose(occupiedDescriptor) == 0 && contents() == "incoming"); + { std::ofstream stream(incoming); stream << "replaced"; } + Require(sceKernelChmod_nid_postfix(occupied.c_str(), 0400) == 0); + Require(replace() == 0 && !std::filesystem::exists(incoming) && contents() == "replaced"); + Require(remove_nid_postfix(occupied.c_str()) == 0); + } #ifndef _WIN32 const auto dangling = area / "dangling"; std::filesystem::create_symlink(area / "nonexistent", dangling); diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp new file mode 100644 index 0000000000..75dd90152b --- /dev/null +++ b/core/libs/tests/GuestFminf.cpp @@ -0,0 +1,46 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +float APS5_VABI fminf_nid_postfix(float, float); +} + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "fminf check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +int main() { + const auto nan = std::numeric_limits::quiet_NaN(); + const auto infinity = std::numeric_limits::infinity(); + + Require(fminf_nid_postfix(3.0f, -2.0f) == -2.0f); + Require(fminf_nid_postfix(-2.0f, 3.0f) == -2.0f); + Require(fminf_nid_postfix(1.5f, 2.5f) == 1.5f); + + Require(fminf_nid_postfix(nan, 4.0f) == 4.0f); + Require(fminf_nid_postfix(4.0f, nan) == 4.0f); + Require(std::isnan(fminf_nid_postfix(nan, nan))); + + const auto minNegZero = fminf_nid_postfix(-0.0f, +0.0f); + const auto minPosZero = fminf_nid_postfix(+0.0f, -0.0f); + Require(minNegZero == 0.0f && std::signbit(minNegZero)); + Require(minPosZero == 0.0f && std::signbit(minPosZero)); + Require(std::signbit(fminf_nid_postfix(-0.0f, -0.0f))); + Require(!std::signbit(fminf_nid_postfix(+0.0f, +0.0f))); + + Require(fminf_nid_postfix(infinity, -infinity) == -infinity); + Require(fminf_nid_postfix(-infinity, infinity) == -infinity); + Require(fminf_nid_postfix(infinity, 5.0f) == 5.0f); + Require(fminf_nid_postfix(5.0f, infinity) == 5.0f); + Require(fminf_nid_postfix(-infinity, 5.0f) == -infinity); + Require(fminf_nid_postfix(5.0f, -infinity) == -infinity); + + return 0; +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 9353846f08..e1f1b5256a 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include extern "C" { int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); +int APS5_VABI clock_settime_nid_postfix(int clockId, const KernelTimespec* tp); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); int* APS5_VABI __error_nid_postfix(); @@ -25,7 +27,9 @@ static constexpr int GUEST_CLOCK_REALTIME = 0; static constexpr int GUEST_CLOCK_VIRTUAL = 1; static constexpr int GUEST_CLOCK_PROF = 2; static constexpr int GUEST_CLOCK_MONOTONIC = 4; +static constexpr int GUEST_CLOCK_REALTIME_PRECISE = 9; static constexpr int GUEST_CLOCK_SECOND = 13; +static constexpr int GUEST_CLOCK_THREAD_CPUTIME_ID = 14; static constexpr int GUEST_CLOCK_PROCESS_CPUTIME_ID = 15; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000LL; @@ -205,9 +209,43 @@ static void CalendarConversionsInitializeTheirState() { Require(sceKernelConvertUtcToLocaltime(0, nullptr, nullptr, nullptr) == SCE_OK); } +static void RequireSettimeError(int clockId, const KernelTimespec* time, int error) { + *__error_nid_postfix() = 0; + Require(clock_settime_nid_postfix(clockId, time) == -1 && *__error_nid_postfix() == error); +} + +static bool SettimeThrows(const KernelTimespec& time, const char* requested) { + try { + clock_settime_nid_postfix(GUEST_CLOCK_REALTIME, &time); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), requested) != nullptr; + } + return false; +} + +static void SettimeValidatesLikeFreebsd() { + const KernelTimespec valid{1767225600, 5}; + RequireSettimeError(GUEST_CLOCK_REALTIME, nullptr, 14); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, nullptr, 14); + for (const int clockId : {GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_PROF, GUEST_CLOCK_MONOTONIC, GUEST_CLOCK_REALTIME_PRECISE, + GUEST_CLOCK_SECOND, GUEST_CLOCK_THREAD_CPUTIME_ID, GUEST_CLOCK_PROCESS_CPUTIME_ID, -1, 16}) + RequireSettimeError(clockId, &valid, 22); + const KernelTimespec negativeNanoseconds{0, -1}; + const KernelTimespec wholeSecondOfNanoseconds{0, NANOS_PER_SECOND}; + const KernelTimespec negativeSeconds{-1, 0}; + RequireSettimeError(GUEST_CLOCK_REALTIME, &negativeNanoseconds, 22); + RequireSettimeError(GUEST_CLOCK_REALTIME, &wholeSecondOfNanoseconds, 22); + RequireSettimeError(GUEST_CLOCK_REALTIME, &negativeSeconds, 22); + RequireSettimeError(GUEST_CLOCK_MONOTONIC, &negativeSeconds, 22); + Require(SettimeThrows(valid, "1767225600.000000005")); + Require(SettimeThrows(KernelTimespec{0, 0}, "0.000000000")); + Require(SettimeThrows(KernelTimespec{1, NANOS_PER_SECOND - 1}, "1.999999999")); +} + int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); + SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; diff --git a/core/libs/tests/GuestFstatfs.cpp b/core/libs/tests/GuestFstatfs.cpp index cc06e5e211..0c86546ccb 100644 --- a/core/libs/tests/GuestFstatfs.cpp +++ b/core/libs/tests/GuestFstatfs.cpp @@ -57,7 +57,7 @@ static constexpr std::uint32_t StatfsVersion = 0x20030518; static constexpr int GuestEbadf = 9; static constexpr int GuestEfault = 14; static constexpr int GuestEinval = 22; -static constexpr int FirstSocket = 0x10000000; +static constexpr int DescriptorSetSize = 1024; static void RequireFailure(int descriptor, int error) { GuestStatfs fs; @@ -116,7 +116,7 @@ int main() { int sockets[2]; Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); - Require(sockets[0] >= FirstSocket); + Require(sockets[0] >= 0 && sockets[0] < DescriptorSetSize); RequireFailure(sockets[0], GuestEinval); Require(close_nid_postfix(sockets[0]) == 0); Require(close_nid_postfix(sockets[1]) == 0); diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp new file mode 100644 index 0000000000..e42958827f --- /dev/null +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -0,0 +1,99 @@ +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct Category { const void* const* vtable; }; +struct ErrorCode { int value; const Category* category; }; +using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); +using Name = const char* (APS5_VABI *)(const Category*); +using Condition = ErrorCode (APS5_VABI *)(const Category*, int); + +extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); +const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const Category* self, int value); +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + +static void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "generic category check failed at line %d\n", line); + std::exit(1); +} +#define REQUIRE(value) Require((value), __LINE__) + +static void CheckMessage(Message message, const Category* category, int value, const char* expected) { + constexpr std::uint64_t marker = 0xA55AA55AA55AA55Aull; + struct Guarded { std::uint64_t before; GuestLocale::String result; std::uint64_t after; }; + Guarded guarded {marker, {}, marker}; + std::memset(&guarded.result, 0xcd, sizeof(guarded.result)); + *__error_nid_postfix() = 61; + REQUIRE(message(&guarded.result, category, value) == &guarded.result); + REQUIRE(*__error_nid_postfix() == 61); + REQUIRE(guarded.before == marker && guarded.after == marker); + const auto length = std::strlen(expected); + REQUIRE(guarded.result.reserved == 0 && guarded.result.size == length); + REQUIRE(guarded.result.capacity >= length && (guarded.result.capacity & 15) == 15); + const char* text = guarded.result.capacity <= 15 ? guarded.result.buffer : guarded.result.pointer; + REQUIRE(std::memcmp(text, expected, length + 1) == 0); + REQUIRE((guarded.result.capacity <= 15) == (length <= 15)); + if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + const Category* category = _ZSt16generic_categoryv_nid_postfix(); + REQUIRE(category && category == _ZSt16generic_categoryv_nid_postfix()); + REQUIRE(std::strcmp(reinterpret_cast(category->vtable[2])(category), "generic") == 0); + const auto message = reinterpret_cast(category->vtable[3]); + const auto condition = reinterpret_cast(category->vtable[4])(category, 35); + REQUIRE(condition.value == 35 && condition.category == category); + + struct Expected { int value; const char* text; }; + const Expected expected[] { + {3, "no such process"}, {14, "bad address"}, {13, "permission denied"}, + {25, "inappropriate io control operation"}, {35, "operation would block"}, + {45, "not supported"}, {47, "address family not supported"}, {60, "timed out"}, + {78, "function not supported"}, {95, "state not recoverable"}, {96, "owner dead"} + }; + for (const auto& error : expected) { + *__error_nid_postfix() = 61; + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(error.value); + REQUIRE(mapped && std::strcmp(mapped, error.text) == 0 && *__error_nid_postfix() == 61); + CheckMessage(message, category, error.value, error.text); + CheckMessage(_ZNKSt23_Generic_error_category7messageEi_nid_postfix, category, error.value, error.text); + } + + unsigned known = 0; + for (int value = 1; value <= 96; ++value) { + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(value); + if (mapped) ++known; + CheckMessage(message, category, value, mapped ? mapped : "unknown error"); + } + REQUIRE(known == 72); + for (int value : {std::numeric_limits::min(), -1, 0, 15, 44, 97, std::numeric_limits::max()}) { + *__error_nid_postfix() = 61; + REQUIRE(_ZSt13_Syserror_mapi_nid_postfix(value) == nullptr && *__error_nid_postfix() == 61); + CheckMessage(message, category, value, "unknown error"); + } + GuestLocale::String first {}, second {}; + REQUIRE(message(&first, category, 25) == &first && message(&second, category, 11) == &second); + REQUIRE(first.pointer != second.pointer); + _ZdlPv_nid_postfix(second.pointer); + REQUIRE(std::strcmp(first.pointer, "inappropriate io control operation") == 0); + _ZdlPv_nid_postfix(first.pointer); + bool rejected = false; + try { message(nullptr, category, 22); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); + std::puts("guest generic category, FreeBSD error map and message ownership: PASS"); +} diff --git a/core/libs/tests/GuestGetopt.cpp b/core/libs/tests/GuestGetopt.cpp new file mode 100644 index 0000000000..fa3fbebf1b --- /dev/null +++ b/core/libs/tests/GuestGetopt.cpp @@ -0,0 +1,367 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct GuestOption { + const char* name; + int hasArgument; + int* flag; + int value; +}; + +extern "C" { +int APS5_VABI getopt_nid_postfix(int, char* const*, const char*); +int APS5_VABI getopt_long_nid_postfix(int, char* const*, const char*, const GuestOption*, int*); +int APS5_VABI setenv_nid_postfix(const char*, const char*, int); +int APS5_VABI unsetenv_nid_postfix(const char*); +extern int opterr_nid_postfix; +extern int optind_nid_postfix; +extern int optopt_nid_postfix; +extern int optreset_nid_postfix; +extern char* optarg_nid_postfix; +extern FileStream* __stderrp_nid_postfix; +} + +namespace { + +void Expect(bool value, const std::string& what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what.c_str()); + std::abort(); +} + +void ExpectText(const std::string& actual, const std::string& expected, const std::string& what) { + Expect(actual == expected, what + ": `" + actual + "' != `" + expected + "'"); +} + +class ArgumentVector { + std::vector storage; + std::vector pointers; + +public: + explicit ArgumentVector(const std::string& line) { + std::istringstream words(line); + for (std::string word; words >> word;) storage.push_back(word); + for (auto& word : storage) pointers.push_back(word.data()); + pointers.push_back(nullptr); + } + int Count() const { return static_cast(storage.size()); } + char* const* Values() { return pointers.data(); } + std::string Joined() const { + std::string result; + for (std::size_t i = 0; i + 1 < pointers.size(); ++i) result += (i ? " " : "") + std::string(pointers[i]); + return result; + } +}; + +class StderrCapture { + FileStream* previous; + FileStream stream; + +public: + StderrCapture() : previous(__stderrp_nid_postfix), stream(std::tmpfile()) { __stderrp_nid_postfix = &stream; } + ~StderrCapture() { + __stderrp_nid_postfix = previous; + stream.Close(); + } + std::string Text() { + std::FILE* handle = stream.GetHandle(); + std::fflush(handle); + std::rewind(handle); + std::string text; + for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast(character)); + return text; + } +}; + +void Reset() { + optind_nid_postfix = optreset_nid_postfix = 1; + opterr_nid_postfix = 1; +} + +std::string Finish(std::string result, int count) { + if (result.empty()) result = "|"; + else result.back() = '|'; + return result + std::to_string(count - optind_nid_postfix); +} + +std::string RunGetopt(const char* options, ArgumentVector& arguments) { + Reset(); + std::string result; + for (int character; (character = getopt_nid_postfix(arguments.Count(), arguments.Values(), options)) != -1;) { + Expect(character != ':', "getopt returned `:'"); + const char* found = std::strchr(options, character); + if (!found) result += "!" + std::string(1, static_cast(character)) + ","; + else if (found[1] != ':') result += std::string(1, static_cast(character)) + ","; + else result += std::string(1, static_cast(character)) + "=" + optarg_nid_postfix + ","; + } + return Finish(result, arguments.Count()); +} + +std::string RunGetoptLong(const char* options, const GuestOption* longOptions, ArgumentVector& arguments) { + Reset(); + std::string result; + int index = -2; + for (int character; (character = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), options, longOptions, &index)) != -1;) { + Expect(character != ':', "getopt_long returned `:'"); + if (index == -2) { + const char* found = std::strchr(options, character); + if (!found) result += "!" + std::string(1, static_cast(character)) + ","; + else if (found[1] != ':') result += std::string(1, static_cast(character)) + ","; + else result += std::string(1, static_cast(character)) + "=" + optarg_nid_postfix + ","; + } else { + const GuestOption& option = longOptions[index]; + result += "-" + std::string(option.name); + if (option.hasArgument == 1) result += "=" + std::string(optarg_nid_postfix); + if (option.hasArgument == 2 && optarg_nid_postfix) result += "=" + std::string(optarg_nid_postfix); + result += ","; + } + index = -2; + } + return Finish(result, arguments.Count()); +} + +struct GetoptCase { + const char* options; + const char* arguments; + const char* result; +}; + +void FreeBsdGetoptCases() { + const GetoptCase cases[] = { + {"c:d", "foo -c 1 -d foo", "c=1,d|1"}, + {"c:d", "foo -d foo bar", "d|2"}, + {"c:d", "foo -c 2 foo bar", "c=2|2"}, + {"c:d", "foo -e 1 foo bar", "!?|3"}, + {"c:d", "foo -d -- -c 1", "d|2"}, + {"c:d", "foo -c- 1", "c=-|1"}, + {"c:d", "foo -d - 1", "d|2"}, + }; + for (const auto& test : cases) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + ExpectText(RunGetopt(test.options, arguments), test.result, std::string("getopt ") + test.arguments); + const std::string expectedError = std::strcmp(test.result, "!?|3") == 0 ? "eboot.bin: illegal option -- e\n" : ""; + ExpectText(capture.Text(), expectedError, std::string("getopt stderr ") + test.arguments); + } +} + +void FreeBsdGetoptLongCases() { + const GuestOption gnu[] = { + {"required", 1, nullptr, 'r'}, + {"optional", 2, nullptr, 'o'}, + {"none", 0, nullptr, 'n'}, + {"color", 0, nullptr, 'C'}, + {"colour", 0, nullptr, 'C'}, + {nullptr, 0, nullptr, 0}, + }; + const GetoptCase cases[] = { + {"abc:", "foo --req foobar", "-required=foobar|0"}, + {"abc:", "foo --opt=bazbug", "-optional=bazbug|0"}, + {"abc:", "foo --col", "-color|0"}, + {"abc:", "foo --colour", "-colour|0"}, + {"abc:", "foo -a -b -cfoobar --required foobar --optional=bazbug --none random --col --color --colour", + "a,b,c=foobar,-required=foobar,-optional=bazbug,-none,-color,-color,-colour|1"}, + }; + for (const auto& test : cases) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + ExpectText(RunGetoptLong(test.options, gnu, arguments), test.result, std::string("getopt_long ") + test.arguments); + ExpectText(capture.Text(), "", std::string("getopt_long stderr ") + test.arguments); + } + + int flags[3] {}; + const GuestOption lists[] = { + {"list-foobar", 0, &flags[0], 'f'}, + {"list-goobar", 0, &flags[1], 'g'}, + {"list", 0, &flags[2], 'l'}, + {nullptr, 0, nullptr, 0}, + }; + ArgumentVector arguments("foo --list"); + ExpectText(RunGetoptLong("fgl", lists, arguments), "-list|0", "exact match among prefixes"); + Expect(flags[0] == 0 && flags[1] == 0 && flags[2] == 'l', "flag set by an exact match"); +} + +void ShortOptions() { + ArgumentVector grouped("foo -dcvalue -dd rest"); + ExpectText(RunGetopt("c:d", grouped), "d,c=value,d,d|1", "grouped options"); + + ArgumentVector optional("foo -afoo -a bar"); + Reset(); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && std::strcmp(optarg_nid_postfix, "foo") == 0, "optional attached"); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == 'a' && optarg_nid_postfix == nullptr && optind_nid_postfix == 3, "optional absent"); + Expect(getopt_nid_postfix(optional.Count(), optional.Values(), "a::") == -1 && optind_nid_postfix == 3, "optional stops at operand"); + + ArgumentVector dash("foo - -x"); + Reset(); + Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == '-' && optind_nid_postfix == 2, "lone dash as option"); + Expect(getopt_nid_postfix(dash.Count(), dash.Values(), "-x") == 'x' && optind_nid_postfix == 3, "after lone dash"); + + ArgumentVector unknown("foo -x"); + { + StderrCapture capture; + Reset(); + opterr_nid_postfix = 0; + Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), "ab") == '?', "unknown option returns ?"); + Expect(optopt_nid_postfix == 'x' && optind_nid_postfix == 2, "optopt of unknown option"); + ExpectText(capture.Text(), "", "opterr 0 is silent"); + } + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(unknown.Count(), unknown.Values(), ":ab") == '?' && optopt_nid_postfix == 'x', "leading colon unknown"); + ExpectText(capture.Text(), "", "leading colon is silent"); + } + + ArgumentVector missing("foo -c"); + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(missing.Count(), missing.Values(), "c:") == '?' && optopt_nid_postfix == 'c' && optind_nid_postfix == 2, "missing argument"); + ExpectText(capture.Text(), "eboot.bin: option requires an argument -- c\n", "missing argument message"); + } + { + StderrCapture capture; + Reset(); + Expect(getopt_nid_postfix(missing.Count(), missing.Values(), ":c:") == ':' && optopt_nid_postfix == 'c', "missing argument with leading colon"); + ExpectText(capture.Text(), "", "leading colon missing argument is silent"); + } + + ArgumentVector first("foo -ab"); + ArgumentVector second("bar -c"); + Reset(); + Expect(getopt_nid_postfix(first.Count(), first.Values(), "abc") == 'a', "first parse"); + optind_nid_postfix = optreset_nid_postfix = 1; + Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == 'c', "optreset restarts getopt"); + Expect(getopt_nid_postfix(second.Count(), second.Values(), "abc") == -1 && optind_nid_postfix == 2, "restarted parse ends"); + + ArgumentVector permuted("foo a -b c"); + ExpectText(RunGetopt("bc", permuted), "|3", "getopt does not permute"); +} + +void LongOptions() { + int flag = 0; + const GuestOption options[] = { + {"required", 1, nullptr, 'r'}, + {"optional", 2, nullptr, 'o'}, + {"none", 0, nullptr, 'n'}, + {"verbose", 0, nullptr, 'v'}, + {"version", 0, nullptr, 'V'}, + {"flag", 0, &flag, 7}, + {nullptr, 0, nullptr, 0}, + }; + + ArgumentVector kinds("foo --required=1 --required 2 --optional=3 --optional 4 --none --flag"); + ExpectText(RunGetoptLong("", options, kinds), "-required=1,-required=2,-optional=3,-optional,-none,-flag|1", "three argument kinds"); + Expect(flag == 7, "flag pointer set"); + ExpectText(kinds.Joined(), "foo --required=1 --required 2 --optional=3 --optional --none --flag 4", "operand permuted to the end"); + + ArgumentVector abbreviations("foo --verb --vers --req=x --opt"); + ExpectText(RunGetoptLong("", options, abbreviations), "-verbose,-version,-required=x,-optional|0", "unique abbreviations"); + + struct ErrorCase { + const char* options; + const char* arguments; + int result; + int optopt; + const char* message; + }; + const ErrorCase errors[] = { + {"", "foo --ver", '?', 0, "eboot.bin: option `--ver' is ambiguous\n"}, + {"", "foo --none=1", '?', 'n', "eboot.bin: option `--none' doesn't allow an argument\n"}, + {"", "foo --flag=1", '?', 0, "eboot.bin: option `--flag' doesn't allow an argument\n"}, + {"", "foo --required", '?', 'r', "eboot.bin: option `--required' requires an argument\n"}, + {":", "foo --required", ':', 'r', ""}, + {"", "foo --nope=1", '?', 0, "eboot.bin: unrecognized option `--nope=1'\n"}, + {"", "foo -x", '?', 'x', "eboot.bin: invalid option -- x\n"}, + {"c:", "foo -c", '?', 'c', "eboot.bin: option requires an argument -- c\n"}, + {":c:", "foo -c", ':', 'c', ""}, + {"W;", "foo -W ver", '?', 0, "eboot.bin: option `-W ver' is ambiguous\n"}, + {"W;", "foo -W", '?', 'W', "eboot.bin: option requires an argument -- W\n"}, + }; + for (const auto& test : errors) { + ArgumentVector arguments(test.arguments); + StderrCapture capture; + Reset(); + optopt_nid_postfix = -1; + const int result = getopt_long_nid_postfix(arguments.Count(), arguments.Values(), test.options, options, nullptr); + Expect(result == test.result, std::string("getopt_long result ") + test.arguments); + Expect(optopt_nid_postfix == test.optopt, std::string("getopt_long optopt ") + test.arguments); + ExpectText(capture.Text(), test.message, std::string("getopt_long message ") + test.arguments); + } + { + ArgumentVector arguments("foo --ver -x"); + StderrCapture capture; + Reset(); + opterr_nid_postfix = 0; + Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 0, "silent ambiguity"); + Expect(getopt_long_nid_postfix(arguments.Count(), arguments.Values(), "", options, nullptr) == '?' && optopt_nid_postfix == 'x', "silent unknown"); + ExpectText(capture.Text(), "", "opterr 0 silences getopt_long"); + } + + ArgumentVector wOption("foo -W verbose -Wversion"); + ExpectText(RunGetoptLong("W;", options, wOption), "-verbose,-version|0", "W; long options"); + + ArgumentVector terminated("foo a -b -- -d c"); + ExpectText(RunGetoptLong("bd", options, terminated), "b|3", "-- ends getopt_long"); + ExpectText(terminated.Joined(), "foo -b -- a -d c", "-- permutes skipped operands"); + + ArgumentVector interleaved("foo a -b c -d"); + ExpectText(RunGetoptLong("bd", options, interleaved), "b,d|2", "permutation"); + ExpectText(interleaved.Joined(), "foo -b -d a c", "permuted order"); + + ArgumentVector plus("foo a -b"); + ExpectText(RunGetoptLong("+b", options, plus), "|2", "+ stops at the first operand"); + + ArgumentVector inOrder("foo a -b c"); + Reset(); + std::string sequence; + for (int character; (character = getopt_long_nid_postfix(inOrder.Count(), inOrder.Values(), "-b", options, nullptr)) != -1;) + sequence += character == 1 ? std::string("1:") + optarg_nid_postfix + "," : std::string(1, static_cast(character)) + ","; + ExpectText(sequence, "1:a,b,1:c,", "- returns operands in order"); + + ArgumentVector restartFirst("foo -bd"); + ArgumentVector restartSecond("bar -b"); + Reset(); + Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long first parse"); + optind_nid_postfix = 0; + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optind 0 restarts getopt_long"); + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == -1 && optind_nid_postfix == 2, "restarted getopt_long ends"); + Reset(); + Expect(getopt_long_nid_postfix(restartFirst.Count(), restartFirst.Values(), "bd", options, nullptr) == 'b', "getopt_long parse before optreset"); + optind_nid_postfix = optreset_nid_postfix = 1; + Expect(getopt_long_nid_postfix(restartSecond.Count(), restartSecond.Values(), "bd", options, nullptr) == 'b', "optreset restarts getopt_long"); + + Expect(setenv_nid_postfix("POSIXLY_CORRECT", "1", 1) == 0, "setenv POSIXLY_CORRECT"); + { + ArgumentVector posix("foo a -b"); + ExpectText(RunGetoptLong("b", options, posix), "|2", "POSIXLY_CORRECT stops at the first operand"); + ArgumentVector illegal("foo -x"); + StderrCapture capture; + Reset(); + Expect(getopt_long_nid_postfix(illegal.Count(), illegal.Values(), "b", options, nullptr) == '?', "POSIXLY_CORRECT unknown option"); + ExpectText(capture.Text(), "eboot.bin: illegal option -- x\n", "POSIXLY_CORRECT message"); + } + Expect(unsetenv_nid_postfix("POSIXLY_CORRECT") == 0, "unsetenv POSIXLY_CORRECT"); + ArgumentVector gnu("foo a -b"); + ExpectText(RunGetoptLong("b", options, gnu), "b|1", "permutation after POSIXLY_CORRECT is unset"); + + Reset(); + Expect(getopt_long_nid_postfix(gnu.Count(), gnu.Values(), nullptr, options, nullptr) == -1, "null option string"); +} + +} + +int main() { + Expect(opterr_nid_postfix == 1 && optind_nid_postfix == 1 && optopt_nid_postfix == 0 && + optreset_nid_postfix == 0 && optarg_nid_postfix == nullptr, "initial values"); + FreeBsdGetoptCases(); + FreeBsdGetoptLongCases(); + ShortOptions(); + LongOptions(); +} diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index 73b23df87b..f597a80211 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #ifndef _WIN32 diff --git a/core/libs/tests/GuestHeapReportForGame.cpp b/core/libs/tests/GuestHeapReportForGame.cpp new file mode 100644 index 0000000000..9238d7bdb1 --- /dev/null +++ b/core/libs/tests/GuestHeapReportForGame.cpp @@ -0,0 +1,44 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI sceLibcInternalBacktraceForGame_nid_postfix(const char* heapName); +void APS5_VABI sceLibcInternalHeapErrorReportForGame_nid_postfix(void* heap, void* block, std::uint32_t error); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stdout, "%s\n", message); + std::abort(); +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-heap-report-" + std::to_string(std::rand()) + ".txt"); + Require(std::freopen(path.string().c_str(), "w", stderr) != nullptr, "stderr redirection failed"); + + char heapName[] = "SceLibcHeap"; + sceLibcInternalBacktraceForGame_nid_postfix(heapName); + sceLibcInternalBacktraceForGame_nid_postfix(nullptr); + int heap = 0; + int block = 0; + sceLibcInternalHeapErrorReportForGame_nid_postfix(&heap, &block, 0x1234u); + std::fflush(stderr); + std::fclose(stderr); + + std::ifstream file(path); + std::stringstream text; + text << file.rdbuf(); + file.close(); + std::filesystem::remove(path); + const std::string report = text.str(); + Require(report.find("[SceLibcHeap] backtrace requested from guest 0x") != std::string::npos, "heap name not reported"); + Require(report.find("[] backtrace requested from guest 0x") != std::string::npos, "null heap name not reported"); + Require(report.find("[libc] heap error 0x1234: heap ") != std::string::npos, "heap error not reported"); + Require(std::string(heapName) == "SceLibcHeap", "heap name modified"); +} diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp new file mode 100644 index 0000000000..72e2ea7025 --- /dev/null +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -0,0 +1,93 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); +int APS5_VABI setvbuf_nid_postfix(FileStream*, char*, int, std::size_t); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fflush_nid_postfix(FileStream*); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI dup2_nid_postfix(int, int); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +extern FileStream* __stderrp_nid_postfix; +} + +static void Require(bool value) { if (!value) std::abort(); } + +enum class Redirect { Duplicate, Reopen }; + +struct Outcome { + std::string host; + int status = 0; +}; + +static void RedirectGuest(Redirect redirect, const std::filesystem::path& probe) { + if (redirect == Redirect::Duplicate) { + int guest[2] = {-1, -1}; + Require(pipe_nid_postfix(guest) == 0); + Require(dup2_nid_postfix(guest[1], 2) == 2); + Require(write_nid_postfix(2, "guest", 5) == 5); + return; + } + Require(freopen_nid_postfix(probe.c_str(), "w", __stderrp_nid_postfix) == __stderrp_nid_postfix); + Require(setvbuf_nid_postfix(__stderrp_nid_postfix, nullptr, 0, 0) == 0); + Require(fwrite_nid_postfix("guest", 1, 5, __stderrp_nid_postfix) == 5); +} + +static Outcome Run(Redirect redirect, const std::filesystem::path& probe, bool terminate) { + int host[2] = {-1, -1}; + Require(::pipe(host) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + if (::dup2(host[1], 2) != 2) ::_exit(70); + ::close(host[1]); + RedirectGuest(redirect, probe); + std::fputs("host\n", stderr); + if (terminate) throw std::runtime_error("report kept"); + if (redirect == Redirect::Reopen && fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(71); + ::_exit(0); + } + ::close(host[1]); + Outcome outcome; + char buffer[4096]; + for (ssize_t count; (count = ::read(host[0], buffer, sizeof(buffer))) > 0;) outcome.host.append(buffer, static_cast(count)); + ::close(host[0]); + Require(::waitpid(child, &outcome.status, 0) == child); + return outcome; +} + +static std::string Contents(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +int main() { + const std::filesystem::path probe = "host_stderr_probe_" + std::to_string(::getpid()) + ".txt"; + for (const auto redirect : {Redirect::Duplicate, Redirect::Reopen}) { + const auto written = Run(redirect, probe, false); + Require(WIFEXITED(written.status) && WEXITSTATUS(written.status) == 0); + Require(written.host == "host\n"); + if (redirect == Redirect::Reopen) Require(Contents(probe) == "guest"); + + const auto terminated = Run(redirect, probe, true); + Require(WIFSIGNALED(terminated.status) && WTERMSIG(terminated.status) == SIGABRT); + Require(terminated.host.rfind("host\n", 0) == 0); + Require(terminated.host.find(" what(): report kept\n") != std::string::npos); + if (redirect == Redirect::Reopen) Require(Contents(probe).find("report kept") == std::string::npos); + } + std::filesystem::remove(probe); + return 0; +} diff --git a/core/libs/tests/GuestIoctlGetsid.cpp b/core/libs/tests/GuestIoctlGetsid.cpp deleted file mode 100644 index 288e0a8c3c..0000000000 --- a/core/libs/tests/GuestIoctlGetsid.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -extern "C" { -int APS5_VABI getsid_nid_postfix(int); -} -static void Require(bool value) { if (!value) std::abort(); } -template -static bool ReportsNotImplemented(Call call, const std::string& message) { - try { - call(); - } catch (const std::runtime_error& error) { - return error.what() == message; - } - return false; -} -int main() { - Require(ReportsNotImplemented([] { getsid_nid_postfix(0); }, "getsid: process sessions not implemented")); - Require(ReportsNotImplemented([] { getsid_nid_postfix(1); }, "getsid: process sessions not implemented")); -} diff --git a/core/libs/tests/GuestItimer.cpp b/core/libs/tests/GuestItimer.cpp new file mode 100644 index 0000000000..8958441978 --- /dev/null +++ b/core/libs/tests/GuestItimer.cpp @@ -0,0 +1,164 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +using Handler = void (APS5_VABI *)(int); +using TerminateHandler = void (*)(); +extern "C" { +int APS5_VABI setitimer_nid_postfix(int, const GuestItimerval*, GuestItimerval*); +int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); +unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +Handler APS5_VABI signal_nid_postfix(int, Handler); +int* APS5_VABI __error_nid_postfix(); +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); +TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::int64_t Micros(const KernelTimeval& time) { + Require(time.tv_sec >= 0 && time.tv_usec >= 0 && time.tv_usec < 1000000, "timeval is not normalized"); + return time.tv_sec * 1000000 + time.tv_usec; +} + +static GuestItimerval Read(int which) { + GuestItimerval current{{11, 22}, {33, 44}}; + *__error_nid_postfix() = 12345; + Require(setitimer_nid_postfix(which, nullptr, ¤t) == 0, "reading the timer failed"); + Require(*__error_nid_postfix() == 12345, "reading the timer changed errno"); + return current; +} + +static void RequireFailure(int which, const GuestItimerval* value, int error, const char* message) { + GuestItimerval previous{{11, 22}, {33, 44}}; + const auto saved = previous; + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(which, value, &previous) == -1 && *__error_nid_postfix() == error, message); + Require(std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a failed setitimer wrote the previous value"); +} + +volatile std::sig_atomic_t delivered = 0; +static void APS5_VABI OnAlarm(int signal) { + delivered = signal; +} + +static void OnTerminate() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + if (type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0 + && std::strstr(error->what(), "it_interval 0.250000 s") != nullptr && delivered == 0) { + std::printf("expiry threw: %s\n", error->what()); + std::fflush(stdout); + std::_Exit(0); + } + std::fprintf(stderr, "unexpected termination at expiry\n"); + std::_Exit(1); +} + +int main() { + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); + const GuestItimerval valid{{0, 0}, {30, 0}}; + for (int invalid : {-1, 3, INT_MIN, INT_MAX}) { + RequireFailure(invalid, &valid, 22, "an unknown timer did not report EINVAL"); + RequireFailure(invalid, nullptr, 22, "an unknown timer read did not report EINVAL"); + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(invalid, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22, "EINVAL is not checked before EFAULT"); + } + for (int which : {0, 1, 2}) { + *__error_nid_postfix() = 0; + Require(setitimer_nid_postfix(which, nullptr, nullptr) == -1 && *__error_nid_postfix() == 14, "a read into null did not report EFAULT"); + for (const GuestItimerval bad : {GuestItimerval{{0, 0}, {-1, 0}}, GuestItimerval{{0, 0}, {0, -1}}, + GuestItimerval{{0, 0}, {0, 1000000}}, GuestItimerval{{0, 0}, {INT_MAX / 2 + 1, 0}}, + GuestItimerval{{-1, 0}, {1, 0}}, GuestItimerval{{0, 1000000}, {1, 0}}, GuestItimerval{{INT_MAX / 2 + 1, 0}, {1, 0}}}) + RequireFailure(which, &bad, 22, "an invalid timeval did not report EINVAL"); + const GuestItimerval disarm{{-5, 7}, {0, 0}}; + GuestItimerval previous{{11, 22}, {33, 44}}; + Require(setitimer_nid_postfix(which, &disarm, &previous) == 0, "disarming with an invalid interval failed"); + Require(Micros(previous.interval) == 0 && Micros(previous.value) == 0, "an idle timer reported a value"); + Require(Micros(Read(which).interval) == 0, "disarming kept the interval"); + } + for (int cpu : {1, 2}) { + GuestItimerval previous{{11, 22}, {33, 44}}; + const auto saved = previous; + bool threw = false; + try { + setitimer_nid_postfix(cpu, &valid, &previous); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && std::memcmp(&previous, &saved, sizeof(previous)) == 0, "a CPU timer was armed"); + Require(Micros(Read(cpu).value) == 0, "a CPU timer kept a value"); + } + bool threw = false; + const GuestItimerval tiny{{0, 999}, {1, 0}}; + try { + setitimer_nid_postfix(0, &tiny, nullptr); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && Micros(Read(0).value) == 0, "an interval below the clock tick was armed"); + + GuestItimerval previous{}; + Require(setitimer_nid_postfix(0, &valid, &previous) == 0 && Micros(previous.value) == 0, "arming ITIMER_REAL failed"); + const auto first = Micros(Read(0).value); + Require(first > 29000000 && first <= 30000000, "the armed value is not visible"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + const auto second = Micros(Read(0).value); + Require(second > 0 && second < first, "the remaining time does not count down"); + const GuestItimerval repeating{{2, 500000}, {20, 0}}; + Require(setitimer_nid_postfix(0, &repeating, &previous) == 0, "re-arming ITIMER_REAL failed"); + Require(Micros(previous.interval) == 0 && Micros(previous.value) > 0 && Micros(previous.value) <= second, "re-arming did not return the previous value"); + GuestItimerval queried{{11, 22}, {33, 44}}; + Require(getitimer_nid_postfix(0, &queried) == 0, "getitimer failed"); + Require(Micros(queried.interval) == 2500000 && Micros(queried.value) > 19000000 && Micros(queried.value) <= 20000000, + "getitimer did not report the setitimer interval"); + const GuestItimerval zero{{0, 0}, {0, 0}}; + Require(setitimer_nid_postfix(0, &zero, &previous) == 0, "disarming ITIMER_REAL failed"); + Require(Micros(previous.interval) == 2500000 && Micros(previous.value) > 19000000 && Micros(previous.value) <= 20000000, + "disarming did not return the previous value"); + const auto idle = Read(0); + Require(Micros(idle.interval) == 0 && Micros(idle.value) == 0, "the disarmed timer is still set"); + + const GuestItimerval periodic{{0, 300000}, {0, 100000}}; + Require(setitimer_nid_postfix(0, &periodic, nullptr) == 0, "arming a periodic timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + const auto rearmed = Read(0); + Require(Micros(rearmed.interval) == 300000 && Micros(rearmed.value) > 0 && Micros(rearmed.value) <= 300000, + "the periodic timer was not re-armed by its interval"); + Require(GuestAlarm_nid_no_patch(5) == 1, "alarm did not report the periodic timer"); + const auto alarmed = Read(0); + Require(Micros(alarmed.interval) == 0 && Micros(alarmed.value) > 4000000 && Micros(alarmed.value) <= 5000000, "alarm did not replace the timer"); + Require(setitimer_nid_postfix(0, &zero, &previous) == 0 && Micros(previous.value) > 0, "disarming the alarm failed"); + Require(GuestAlarm_nid_no_patch(0) == 0, "setitimer did not cancel the alarm"); + + Require(signal_nid_postfix(14, OnAlarm) == ignore, "SIGALRM handler was not installed"); + _ZSt13set_terminatePFvvE_nid_postfix(OnTerminate); + const GuestItimerval handled{{0, 250000}, {0, 50000}}; + Require(setitimer_nid_postfix(0, &handled, nullptr) == 0, "arming the handled timer failed"); + std::this_thread::sleep_for(std::chrono::seconds(2)); + std::fprintf(stderr, "the expiry with a handler neither delivered nor threw\n"); + return 1; +} diff --git a/core/libs/tests/GuestJitSharedMemory.cpp b/core/libs/tests/GuestJitSharedMemory.cpp new file mode 100644 index 0000000000..c1c966ee0c --- /dev/null +++ b/core/libs/tests/GuestJitSharedMemory.cpp @@ -0,0 +1,174 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelJitCreateSharedMemory(const char*, std::size_t, int, int*); +int APS5_VABI sceKernelJitCreateAliasOfSharedMemory(int, int, int*); +int APS5_VABI sceKernelJitMapSharedMemory(int, int, void**); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +} + +namespace { + +constexpr std::size_t Size = 0x10000; +constexpr int Read = 0x1; +constexpr int Write = 0x2; +constexpr int Execute = 0x4; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "requirement failed at line %u\n", static_cast(location.line())); + std::abort(); +} + +int Create(const char* name, int maxProt) { + int fd = -1; + Require(sceKernelJitCreateSharedMemory(name, Size, maxProt, &fd) == 0); + Require(fd >= 0); + return fd; +} + +void* Map(int fd, int prot, void* hint = nullptr) { + void* address = hint; + Require(sceKernelJitMapSharedMemory(fd, prot, &address) == 0); + Require(address != nullptr && (reinterpret_cast(address) & 0x3fff) == 0); + return address; +} + +int Call(const void* code) { + using Function = int (*)(); + Function function; + std::memcpy(&function, &code, sizeof(function)); + return function(); +} + +void WriteReturn(void* code, std::uint32_t value) { + unsigned char bytes[] = {0xb8, 0, 0, 0, 0, 0xc3}; + std::memcpy(bytes + 1, &value, sizeof(value)); + std::memcpy(code, bytes, sizeof(bytes)); +} + +void WriteThroughAliasRunsThroughExecutableView() { + const int fd = Create(nullptr, Read | Write | Execute); + int alias = -1; + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read | Write, &alias) == 0); + Require(alias >= 0 && alias != fd); + auto* writable = static_cast(Map(alias, Read | Write)); + auto* executable = static_cast(Map(fd, Read | Execute)); + Require(writable != executable); + Require(executable[0] == 0 && executable[Size - 1] == 0); + WriteReturn(writable, 0x12345678); + Require(std::memcmp(writable, executable, 6) == 0); + Require(Call(executable) == 0x12345678); + WriteReturn(writable + 0x4000, 0x0badcafe); + Require(Call(executable + 0x4000) == 0x0badcafe); + writable[Size - 1] = 0x5a; + Require(executable[Size - 1] == 0x5a); + Require(close_nid_postfix(fd) == 0); + auto* again = static_cast(Map(alias, Read)); + Require(again[Size - 1] == 0x5a); + Require(sceKernelMunmap(again, Size) == 0); + Require(sceKernelMunmap(writable, Size) == 0); + Require(Call(executable) == 0x12345678); + Require(sceKernelMunmap(executable, Size) == 0); + Require(close_nid_postfix(alias) == 0); +} + +void OneViewWritesAndRuns() { + const int fd = Create("ps5-platform-probe", Read | Write | Execute); + void* hint = reinterpret_cast(std::uintptr_t{0x1000000000}); + auto* code = static_cast(Map(fd, Read | Write | Execute, hint)); + Require(reinterpret_cast(code) >= 0x1000000000); + WriteReturn(code, 42); + Require(Call(code) == 42); + const int duplicate = dup_nid_postfix(fd); + Require(duplicate >= 0); + Require(close_nid_postfix(fd) == 0); + auto* view = static_cast(Map(duplicate, Read | Execute)); + Require(Call(view) == 42); + Require(sceKernelMunmap(view, Size) == 0); + Require(sceKernelMunmap(code, Size) == 0); + Require(close_nid_postfix(duplicate) == 0); +} + +void Dup2KeepsTheDescriptorTableInStep() { + const int fd = Create(nullptr, Read | Write); + const int target = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); + Require(target >= 0 && target != fd); + Require(dup2_nid_postfix(fd, target) == target); + auto* writable = static_cast(Map(fd, Read | Write)); + auto* readable = static_cast(Map(target, Read)); + writable[0x100] = 0x77; + Require(readable[0x100] == 0x77); + void* address = nullptr; + Require(sceKernelJitMapSharedMemory(target, Read | Execute, &address) == SCE_KERNEL_ERROR_EACCES); + Require(dup2_nid_postfix(fd, fd) == fd); + auto* same = static_cast(Map(fd, Read)); + Require(same[0x100] == 0x77); + const int plain = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); + Require(plain >= 0); + Require(dup2_nid_postfix(plain, target) == target); + address = nullptr; + Require(sceKernelJitMapSharedMemory(target, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(address == nullptr); + Require(sceKernelMunmap(same, Size) == 0); + Require(sceKernelMunmap(readable, Size) == 0); + Require(sceKernelMunmap(writable, Size) == 0); + Require(close_nid_postfix(plain) == 0); + Require(close_nid_postfix(target) == 0); + Require(close_nid_postfix(fd) == 0); +} + +void InvalidArgumentsAreRejected() { + int fd = -1; + Require(sceKernelJitCreateSharedMemory(nullptr, 0, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, 0x1000, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size + 0x2000, Read | Write | Execute, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size, 0x10, &fd) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateSharedMemory(nullptr, Size, Read | Write | Execute, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(fd == -1); + + fd = Create(nullptr, Read | Execute); + int alias = -1; + void* address = nullptr; + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read | Write, &alias) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, 0x20, &alias) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitCreateAliasOfSharedMemory(-1, Read, &alias) == SCE_KERNEL_ERROR_EBADF); + Require(alias == -1); + Require(sceKernelJitMapSharedMemory(fd, Read | Write, &address) == SCE_KERNEL_ERROR_EACCES); + Require(sceKernelJitMapSharedMemory(fd, 0x8, &address) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitMapSharedMemory(fd, Read, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelJitMapSharedMemory(-1, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(address == nullptr); + Require(sceKernelJitCreateAliasOfSharedMemory(fd, Read, &alias) == 0); + Require(sceKernelJitMapSharedMemory(alias, Read | Execute, &address) == SCE_KERNEL_ERROR_EACCES); + Require(close_nid_postfix(alias) == 0); + Require(close_nid_postfix(fd) == 0); + Require(sceKernelJitMapSharedMemory(fd, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelJitCreateAliasOfSharedMemory(alias, Read, &alias) == SCE_KERNEL_ERROR_EBADF); + + const int shared = shm_open_nid_postfix(reinterpret_cast(1), 0x2, 0600); + Require(shared >= 0); + Require(sceKernelJitMapSharedMemory(shared, Read, &address) == SCE_KERNEL_ERROR_EBADF); + Require(close_nid_postfix(shared) == 0); +} + +} + +int main() { + WriteThroughAliasRunsThroughExecutableView(); + OneViewWritesAndRuns(); + Dup2KeepsTheDescriptorTableInStep(); + InvalidArgumentsAreRejected(); +} diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index c2c6030b35..a839d3d034 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -4,8 +4,11 @@ #include #include #include +#include +#include #include #include +#include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); @@ -26,8 +29,12 @@ static constexpr int GetDescriptorFlags = 1; static constexpr int SetDescriptorFlags = 2; static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; +static constexpr int OldGetLock = 7; +static constexpr int OldSetLock = 8; +static constexpr int OldSetLockWait = 9; static constexpr int GetLock = 11; static constexpr int SetLock = 12; +static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; @@ -39,11 +46,19 @@ static constexpr int Append = 8; static constexpr int ReadAheadFlag = 0x200; static constexpr int CreateReadWrite = 0x202; static constexpr int CreateWriteAppend = 0x209; +static constexpr int OpenDirectory = 0x20000; +static constexpr std::int16_t ReadLock = 1; +static constexpr std::int16_t Unlock = 2; +static constexpr std::int16_t WriteLock = 3; +static constexpr std::int16_t FromStart = 0; +static constexpr std::int16_t FromCurrent = 1; +static constexpr std::int16_t FromEnd = 2; static constexpr int GuestEbadf = 9; static constexpr int GuestEfault = 14; static constexpr int GuestEinval = 22; static constexpr int GuestEnotty = 25; static constexpr int GuestEagain = 35; +static constexpr int GuestEoverflow = 84; static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; static constexpr std::uint64_t WindowSizeRequest = 0x40087468; @@ -141,10 +156,6 @@ static void FileFlagsRoundTrip() { Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); RequireFailure(fcntl_nid_postfix(file, 99), GuestEinval); RequireFailure(fcntl_nid_postfix(file, GetLock, nullptr), GuestEfault); -#ifdef _WIN32 - GuestLock lock{0, 0, 0, 3, 0, 0}; - Require(ThrowsWith([file, &lock] { fcntl_nid_postfix(file, SetLock, &lock); }, "fcntl: record locks are not implemented on Windows")); -#else GuestLock lock{0, 0, 0, 3, 0, 0}; Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); GuestLock probe{0, 0, 0, 1, 0, 0}; @@ -153,7 +164,6 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); lock.type = 7; RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); -#endif for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); @@ -177,6 +187,137 @@ static void FileFlagsRoundTrip() { std::remove("kernel_fcntl_probe.bin"); } +static bool NoConflict(int fd, GuestLock request) { + const GuestLock asked = request; + if (fcntl_nid_postfix(fd, GetLock, &request) != 0) return false; + return request.type == Unlock && request.start == asked.start && request.length == asked.length && request.pid == asked.pid && request.whence == asked.whence && request.system == asked.system; +} + +static void RecordLocksBelongToTheProcess() { + const int file = sceKernelOpen("kernel_fcntl_locks.bin", CreateReadWrite, 0644); + Require(file >= 0); + Require(write_nid_postfix(file, "0123456789", 10) == 10); + + GuestLock whole{0, 0, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {4, 2, 0, WriteLock, FromStart, 77})); + GuestLock middle{2, 3, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &middle) == 0); + GuestLock tail{5, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &tail) == 0); + GuestLock merged{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLockWait, &merged) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + + const int other = sceKernelOpen("kernel_fcntl_locks.bin", ReadWrite, 0); + Require(other >= 0); + GuestLock same{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(other, SetLock, &same) == 0); + Require(fcntl_nid_postfix(other, SetLockWait, &same) == 0); + Require(NoConflict(other, {0, 0, 0, WriteLock, FromStart, 0})); + + int threadSet = -1; + int threadWait = -1; + bool threadSeesNoConflict = false; + std::thread thread([&] { + GuestLock request{3, 4, 0, WriteLock, FromStart, 0}; + threadSet = fcntl_nid_postfix(other, SetLock, &request); + threadWait = fcntl_nid_postfix(file, SetLockWait, &request); + threadSeesNoConflict = NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0}); + }); + thread.join(); + Require(threadSet == 0 && threadWait == 0 && threadSeesNoConflict); + + Require(sceKernelLseek(file, 6, 0) == 6); + GuestLock current{-2, 0, 0, ReadLock, FromCurrent, 5}; + Require(fcntl_nid_postfix(file, GetLock, ¤t) == 0); + Require(current.type == Unlock && current.start == 4 && current.whence == FromCurrent && current.length == 0 && current.system == 5); + GuestLock end{-3, 0, 0, WriteLock, FromEnd, 0}; + Require(fcntl_nid_postfix(file, SetLock, &end) == 0); + GuestLock backwards{5, -5, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &backwards) == 0); + + unsigned char old[32]; + std::memset(old, 0xa5, sizeof(old)); + GuestLock oldLock{0, 1, 0, WriteLock, FromStart, 0}; + std::memcpy(old, &oldLock, 24); + Require(fcntl_nid_postfix(file, OldSetLock, old) == 0); + Require(fcntl_nid_postfix(file, OldSetLockWait, old) == 0); + Require(fcntl_nid_postfix(file, OldGetLock, old) == 0); + std::memcpy(&oldLock, old, 24); + Require(oldLock.type == Unlock && oldLock.start == 0 && oldLock.length == 1); + for (std::size_t index = 24; index < sizeof(old); ++index) Require(old[index] == 0xa5); + + GuestLock badType{0, 0, 0, 0, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 4; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 7; + RequireFailure(fcntl_nid_postfix(file, GetLock, &badType), GuestEinval); + GuestLock negative{-1, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &negative), GuestEinval); + GuestLock beforeStart{0, -1, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &beforeStart), GuestEinval); + GuestLock pastStart{5, -6, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastStart), GuestEinval); + GuestLock unknownWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &unknownWhence), GuestEinval); + Require(sceKernelFcntl(file, SetLock, &unknownWhence) == SceEinval); + + const int reader = sceKernelOpen("kernel_fcntl_locks.bin", ReadOnly, 0); + Require(reader >= 0); + GuestLock write{0, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &write), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, SetLockWait, &write), GuestEbadf); + Require(sceKernelFcntl(reader, SetLock, &write) == SceEbadf); + Require(NoConflict(reader, write)); + GuestLock read{0, 0, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &read) == 0); + const int writer = sceKernelOpen("kernel_fcntl_locks.bin", WriteOnly, 0); + Require(writer >= 0); + RequireFailure(fcntl_nid_postfix(writer, SetLock, &read), GuestEbadf); + Require(fcntl_nid_postfix(writer, SetLock, &write) == 0); + GuestLock release{0, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(writer, SetLock, &release) == 0); + Require(fcntl_nid_postfix(reader, SetLock, &release) == 0); + +#ifdef _WIN32 + Require(NoConflict(file, {0, 0, 0, Unlock, FromStart, 0})); + GuestLock tooLong{std::numeric_limits::max(), 2, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &tooLong), GuestEoverflow); + RequireFailure(fcntl_nid_postfix(file, GetLock, &tooLong), GuestEoverflow); + GuestLock pastEnd{std::numeric_limits::max(), 0, 0, WriteLock, FromEnd, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastEnd), GuestEoverflow); + GuestLock pastCurrent{std::numeric_limits::max(), 0, 0, ReadLock, FromCurrent, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &pastCurrent) == 0); + pastCurrent.type = WriteLock; + Require(sceKernelLseek(reader, 1, 0) == 1); + RequireFailure(fcntl_nid_postfix(reader, SetLock, &pastCurrent), GuestEoverflow); + GuestLock writeBeforeWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &writeBeforeWhence), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, GetLock, &writeBeforeWhence), GuestEinval); + + std::filesystem::create_directory("kernel_fcntl_locks_dir"); + const int directory = sceKernelOpen("kernel_fcntl_locks_dir", OpenDirectory, 0); + Require(directory >= 0); + Require(fcntl_nid_postfix(directory, SetLock, &read) == 0); + RequireFailure(fcntl_nid_postfix(directory, SetLock, &write), GuestEbadf); + Require(NoConflict(directory, {0, 0, 0, WriteLock, FromCurrent, 0})); + GuestLock directoryEnd{0, 0, 0, ReadLock, FromEnd, 0}; + Require(ThrowsWith([directory, &directoryEnd] { fcntl_nid_postfix(directory, SetLock, &directoryEnd); }, "fcntl: a record lock from the end on a directory is not implemented")); + Require(close_nid_postfix(directory) == 0); + std::filesystem::remove("kernel_fcntl_locks_dir"); +#endif + + for (const int descriptor : {writer, reader, other}) Require(close_nid_postfix(descriptor) == 0); + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + Require(close_nid_postfix(file) == 0); + RequireFailure(fcntl_nid_postfix(file, SetLock, &whole), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, GetLock, &whole), GuestEbadf); + std::remove("kernel_fcntl_locks.bin"); +} + static void PipeFlagsRoundTrip() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); @@ -236,6 +377,7 @@ static void StandardInputFlags() { int main() { SocketsKeepTheirPath(); FileFlagsRoundTrip(); + RecordLocksBelongToTheProcess(); PipeFlagsRoundTrip(); StandardInputFlags(); return 0; diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp index b2c1097d2a..941ffdc975 100644 --- a/core/libs/tests/GuestKernelIsStack.cpp +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -9,9 +9,11 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); int APS5_VABI sceKernelMapFlexibleMemory(void** addr, std::size_t len, int prot, int flags); int APS5_VABI sceKernelMunmap(std::uint64_t addr, std::size_t len); +int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, std::uint64_t infoSize); } static constexpr int SCE_OK = 0; +static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000000FC00000000ull; static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); static int global = 0; @@ -24,6 +26,8 @@ struct WorkerContext { void* start = nullptr; void* end = nullptr; int result = -1; + VirtualQueryInfo region{}; + int regionResult = -1; }; static void* APS5_VABI Worker(void* arg) { @@ -31,6 +35,7 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.local = &local; context.result = sceKernelIsStack(&local, &context.start, &context.end); + context.regionResult = sceKernelVirtualQuery(&local, 0, &context.region, sizeof(context.region)); context.queried.store(true); while (!context.release.load()) {} return nullptr; @@ -55,6 +60,11 @@ int main() { while (!context.queried.load()) {} Require(context.result == SCE_OK); Require(context.start <= context.local && context.local < context.end); + Require(context.regionResult == SCE_OK); + Require(context.region.start <= reinterpret_cast(context.local) && reinterpret_cast(context.local) < context.region.end); +#if defined(__linux__) + Require(reinterpret_cast(context.local) < APPLICATION_AREA_END); +#endif Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); Require(context.end <= start || context.start >= end); void* workerStart = nullptr; diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index 66aed40472..f946e321ae 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -24,7 +24,7 @@ static constexpr std::uintptr_t Page = 0x4000; static constexpr std::size_t GuardBytes = 0x10000; static constexpr std::size_t HostBytes = 0x40000; -static void Require(bool value) { if (!value) std::abort(); } +#define Require(value) do { if (!(value)) { std::fprintf(stderr, "GuestKernelStackProtect failed at line %d\n", __LINE__); std::abort(); } } while (false) struct PageState { bool mapped; @@ -81,6 +81,18 @@ static void* APS5_VABI Worker(void* arg) { Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); const auto low = (reinterpret_cast(start) + Page - 1) & ~(Page - 1); Require(low + GuardBytes + Page < reinterpret_cast(&local)); +#ifdef __linux__ + std::uintptr_t hostGuard = 0; + for (auto cursor = low; cursor >= Page && low - cursor <= 4 * 1024 * 1024; cursor -= Page) { + const auto page = QueryPage(cursor); + if (!page.mapped) break; + if (!page.writable) { hostGuard = cursor; break; } + } + Require(hostGuard != 0); + const auto hostGuardBefore = QueryPage(hostGuard).raw; + Require(mprotect_nid_postfix(reinterpret_cast(hostGuard), Page, 3) == -1); + Require(QueryPage(hostGuard).raw == hostGuardBefore); +#endif auto* guard = reinterpret_cast(low); Require(mprotect_nid_postfix(const_cast(guard), GuardBytes, 0) == 0); #ifdef _WIN32 @@ -152,7 +164,11 @@ int main() { Require(scePthreadJoin(thread, nullptr) == SCE_OK); for (std::uintptr_t offset = 0; offset < GuardBytes; offset += Page) { const auto left = QueryPage(leftGuard.load() + offset); +#ifdef __linux__ + Require(!left.mapped || left.writable || left.raw == 1); +#else Require(!left.mapped || left.writable); +#endif } Require(scePthreadCreate(&thread, nullptr, WriteLeftGuard, nullptr, nullptr) == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 8483b1be80..5f92999d29 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -27,8 +27,10 @@ int APS5_VABI _fstatfs_nid_postfix(int, void*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); +std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); +int APS5_VABI chroot_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); @@ -36,6 +38,11 @@ int APS5_VABI setsid_nid_postfix(void); int APS5_VABI link_nid_postfix(const char*, const char*); int APS5_VABI symlink_nid_postfix(const char*, const char*); int APS5_VABI openat_nid_postfix(int, const char*, int, ...); +int APS5_VABI unlinkat_nid_postfix(int, const char*, int); +std::int64_t APS5_VABI readlinkat_nid_postfix(int, const char*, char*, std::size_t); +int APS5_VABI mkdirat_nid_postfix(int, const char*, std::uint16_t); +int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); +int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); int* APS5_VABI __error_nid_postfix(); @@ -52,6 +59,57 @@ int Error() { return *__error_nid_postfix(); } +bool PathconfThrows(const char* path, int name) { + try { + pathconf_nid_postfix(path, name); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +bool ChrootThrows(const std::string& path) { + try { + chroot_nid_postfix(path.c_str()); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), path.c_str()) != nullptr; + } + return false; +} + +void CheckPathconf(const std::filesystem::path& root, const std::string& file) { + const auto directory = root.generic_string(); + for (const auto& path : {file, directory}) { + Require(pathconf_nid_postfix(path.c_str(), 1) == 32767); + Require(pathconf_nid_postfix(path.c_str(), 4) == 255); + Require(pathconf_nid_postfix(path.c_str(), 5) == 1024); + Require(pathconf_nid_postfix(path.c_str(), 6) == 512); + Require(pathconf_nid_postfix(path.c_str(), 7) == 1); + Require(pathconf_nid_postfix(path.c_str(), 8) == 1); + Require(pathconf_nid_postfix(path.c_str(), 12) == 64); + *__error_nid_postfix() = 0; + Require(pathconf_nid_postfix(path.c_str(), 15) == -1 && Error() == 0); + Require(pathconf_nid_postfix(path.c_str(), 18) == 1024); + Require(pathconf_nid_postfix(path.c_str(), 54) == 0); + Require(pathconf_nid_postfix(path.c_str(), 55) == 0); + } + for (const int name : {0, 11, 61, 62, 65, -1}) + Require(pathconf_nid_postfix(file.c_str(), name) == -1 && Error() == 22); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(PathconfThrows(file.c_str(), name)); + const auto missing = (root / "missing").string(); + Require(pathconf_nid_postfix(missing.c_str(), 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix(missing.c_str(), 0) == -1 && Error() == 2); + Require(pathconf_nid_postfix((root / "missing" / "file").string().c_str(), 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix("", 4) == -1 && Error() == 2); + Require(pathconf_nid_postfix(nullptr, 4) == -1 && Error() == 14); + const auto longName = directory + "/" + std::string(256, 'n'); + Require(pathconf_nid_postfix(longName.c_str(), 4) == -1 && Error() == 63); + Require(pathconf_nid_postfix((directory + "/missing/" + std::string(256, 'n')).c_str(), 4) == -1 && Error() == 2); + const auto longPath = directory + "/" + std::string(1024, 'p'); + Require(pathconf_nid_postfix(longPath.c_str(), 4) == -1 && Error() == 63); +} + int main() { Require(setsid_nid_postfix() == -1 && Error() == 1); Require(umask_nid_postfix(0) == 022); @@ -62,6 +120,7 @@ int main() { Require(std::filesystem::create_directory(root)); const auto file = (root / "file.txt").string(); std::ofstream(file).put('x'); + CheckPathconf(root, file); const int descriptor = open_nid_postfix(file.c_str(), 0, 0); Require(descriptor >= 0); unsigned char viaPublic[472] = {}; @@ -105,6 +164,18 @@ int main() { Require(readlink_nid_postfix(link.c_str(), nullptr, 0) == 0); Require(readlink_nid_postfix(link.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); Require(readlink_nid_postfix((root / "LINK").string().c_str(), buffer, sizeof(buffer)) == 14); + const int linkDir = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(linkDir >= 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlinkat_nid_postfix(linkDir, "link", buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlinkat_nid_postfix(linkDir, "link", buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlinkat_nid_postfix(linkDir, "link", nullptr, 0) == 0); + Require(readlinkat_nid_postfix(linkDir, "link", nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto guestLink = "/" + root.filename().string() + "/link"; + Require(readlinkat_nid_postfix(-1, guestLink.c_str(), buffer, sizeof(buffer)) == 14 && std::memcmp(buffer, "sub/target.txt", 14) == 0); + Require(readlinkat_nid_postfix(-100, link.c_str(), buffer, sizeof(buffer)) == 14 && std::memcmp(buffer, "sub/target.txt", 14) == 0); + Require(sceKernelClose(linkDir) == 0); #ifndef _WIN32 std::filesystem::create_symlink("loopB", root / "loopA"); std::filesystem::create_symlink("loopA", root / "loopB"); @@ -155,6 +226,39 @@ int main() { Require(sceKernelClose(directory) == 0); Require(fchdir_nid_postfix(directory) == -1 && Error() == 9); Require(chdir_nid_postfix("/") == 0); + Require(chroot_nid_postfix(nullptr) == -1 && Error() == 14); + Require(chroot_nid_postfix("") == -1 && Error() == 2); + Require(chroot_nid_postfix((root / "missing").string().c_str()) == -1 && Error() == 2); + Require(chroot_nid_postfix(file.c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / "file.txt" / "child").string().c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((file + "/").c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / "file.txt" / "..").string().c_str()) == -1 && Error() == 20); + Require(chroot_nid_postfix((root / std::string(256, 'n')).string().c_str()) == -1 && Error() == 63); + Require(chroot_nid_postfix(("/" + std::string(1024, 'p')).c_str()) == -1 && Error() == 63); + Require(ChrootThrows(root.string())); + Require(ChrootThrows("/" + root.filename().string() + "/.")); + const auto guestRoot = "/" + root.filename().string(); + const auto chrootFile = guestRoot + "/file.txt"; + const int top = sceKernelOpen("/", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(top >= 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(fchdir_nid_postfix(top) == 0 && chroot_nid_postfix(".") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); + Require(sceKernelClose(top) == 0); + Require(chroot_nid_postfix("/") == 0 && chroot_nid_postfix("/..") == 0 && chroot_nid_postfix("//./") == 0); + Require(chdir_nid_postfix(guestRoot.c_str()) == 0); + Require(chroot_nid_postfix("..") == 0 && chroot_nid_postfix("../..") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && guestRoot == directoryPath); + Require(ChrootThrows(".")); + for (const auto& path : {std::string("file.txt"), chrootFile, "/../../.." + chrootFile, "../.." + chrootFile}) { + const int opened = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY, 0); + char byte = 0; + Require(opened >= 0 && sceKernelRead(opened, &byte, 1) == 1 && byte == 'x'); + Require(sceKernelClose(opened) == 0); + } + Require(chdir_nid_postfix("/") == 0); + Require(getcwd_nid_postfix(directoryPath, sizeof(directoryPath)) == directoryPath && std::strcmp(directoryPath, "/") == 0); + Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); const auto symbolic = (root / "symbolic").string(); const int created = symlink_nid_postfix("original.bin", symbolic.c_str()); if (created == 0) { @@ -227,10 +331,139 @@ int main() { const int plain = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(plain >= 0); Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); + Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(plain, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 20); + Require(readlinkat_nid_postfix(-1, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 9); + Require(readlinkat_nid_postfix(base, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + Require(readlinkat_nid_postfix(base, "", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(-1, guestFile.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); + Require(mkdirat_nid_postfix(base, "dir", 0700) == 0 && std::filesystem::is_directory(root / "dir")); + Require(mkdirat_nid_postfix(base, "dir", 0700) == -1 && Error() == 17); + Require(mkdirat_nid_postfix(base, "file.txt", 0700) == -1 && Error() == 17); + Require(mkdirat_nid_postfix(base, "missing/child", 0700) == -1 && Error() == 2); + Require(mkdirat_nid_postfix(base, "file.txt/child", 0700) == -1 && Error() == 20); + Require(mkdirat_nid_postfix(base, "slash_dir/", 0700) == 0 && std::filesystem::is_directory(root / "slash_dir")); + const auto guestSubdir = "/" + root.filename().string() + "/abs_dir"; + Require(mkdirat_nid_postfix(-1, guestSubdir.c_str(), 0700) == 0 && std::filesystem::is_directory(root / "abs_dir")); + const auto cwdDir = (root / "cwd_dir").string(); + Require(mkdirat_nid_postfix(-100, cwdDir.c_str(), 0700) == 0 && std::filesystem::is_directory(root / "cwd_dir")); + Require(mkdirat_nid_postfix(plain, "child", 0700) == -1 && Error() == 20); + Require(mkdirat_nid_postfix(-1, "child", 0700) == -1 && Error() == 9); + Require(mkdirat_nid_postfix(base, nullptr, 0700) == -1 && Error() == 14); + Require(mkdirat_nid_postfix(base, "", 0700) == -1 && Error() == 2); + const std::string longName(256, 'a'); + Require(mkdirat_nid_postfix(base, longName.c_str(), 0700) == -1 && Error() == 63); + Require(mkdirat_nid_postfix(base, "dir/../dotdot_dir", 0700) == 0 && std::filesystem::is_directory(root / "dotdot_dir")); + Require(unlinkat_nid_postfix(plain, "file.txt", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(-1, "file.txt", 0) == -1 && Error() == 9); Require(sceKernelClose(plain) == 0); + Require(unlinkat_nid_postfix(base, "file.txt", 1) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, nullptr, 1) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, nullptr, 0) == -1 && Error() == 14); + Require(unlinkat_nid_postfix(base, "", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing.bin", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing.bin", 0x800) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "missing/child", 0) == -1 && Error() == 2); + Require(unlinkat_nid_postfix(base, "file.txt/child", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, "file.txt/child", 0x800) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, "file.txt/", 0) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, longName.c_str(), 0) == -1 && Error() == 63); + Require(unlinkat_nid_postfix(base, "file.txt", 0x800) == -1 && Error() == 20); + Require(unlinkat_nid_postfix(base, ".", 0x800) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, "..", 0x800) == -1 && Error() == 22); + Require(unlinkat_nid_postfix(base, ".", 0) == -1 && Error() == 1); + std::filesystem::create_directory(root / "usub"); + Require(unlinkat_nid_postfix(base, "usub", 0) == -1 && Error() == 1); + std::filesystem::create_directory(root / "nonempty"); + { std::ofstream out(root / "nonempty" / "f.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == -1 && Error() == 66); + Require(unlinkat_nid_postfix(base, "nonempty/f.bin", 0) == 0 && !std::filesystem::exists(root / "nonempty" / "f.bin")); + Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == 0 && !std::filesystem::exists(root / "nonempty")); + { std::ofstream out(root / "to_unlink.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "to_unlink.bin", 0) == 0 && !std::filesystem::exists(root / "to_unlink.bin")); + { std::ofstream out(root / "dotdot.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(base, "usub/../dotdot.bin", 0) == 0 && !std::filesystem::exists(root / "dotdot.bin")); + std::filesystem::create_directory(root / "uslash_sub"); + Require(unlinkat_nid_postfix(base, "uslash_sub/", 0x800) == 0 && !std::filesystem::exists(root / "uslash_sub")); + Require(unlinkat_nid_postfix(base, "usub", 0x800) == 0 && !std::filesystem::exists(root / "usub")); + { std::ofstream out(root / "abs.bin"); out << 'x'; } + const auto guestAbsFile = "/" + root.filename().string() + "/abs.bin"; + Require(unlinkat_nid_postfix(-1, guestAbsFile.c_str(), 0) == 0 && !std::filesystem::exists(root / "abs.bin")); + std::filesystem::create_directory(root / "unlink_abs_dir"); + const auto guestAbsDir = "/" + root.filename().string() + "/unlink_abs_dir"; + Require(unlinkat_nid_postfix(-1, guestAbsDir.c_str(), 0x800) == 0 && !std::filesystem::exists(root / "unlink_abs_dir")); + { std::ofstream out(root / "cwd.bin"); out << 'x'; } + Require(unlinkat_nid_postfix(-100, (root / "cwd.bin").string().c_str(), 0) == 0 && !std::filesystem::exists(root / "cwd.bin")); + std::filesystem::create_directory(root / "unlink_cwd_dir"); + Require(unlinkat_nid_postfix(-100, (root / "unlink_cwd_dir").string().c_str(), 0x800) == 0 && !std::filesystem::exists(root / "unlink_cwd_dir")); Require(openat_nid_postfix(-1, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 9); Require(openat_nid_postfix(base, nullptr, SCE_KERNEL_O_RDONLY) == -1 && Error() == 14); Require(openat_nid_postfix(base, "", SCE_KERNEL_O_RDONLY) == -1 && Error() == 2); + Require(renameat_nid_postfix(base, nullptr, base, "renamed.txt") == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "made.bin", base, nullptr) == -1 && Error() == 14); + Require(renameat_nid_postfix(base, "", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "") == -1 && Error() == 2); + const int bad = sceKernelOpen(file.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(bad >= 0); + Require(renameat_nid_postfix(-1, "made.bin", base, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(base, "made.bin", -1, "renamed.txt") == -1 && Error() == 9); + Require(renameat_nid_postfix(bad, "made.bin", base, "renamed.txt") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "made.bin", bad, "renamed.txt") == -1 && Error() == 20); + Require(sceKernelClose(bad) == 0); + const auto longPart = std::string(256, 'y'); + Require(renameat_nid_postfix(base, longPart.c_str(), base, "renamed.txt") == -1 && Error() == 63); + Require(renameat_nid_postfix(base, "made.bin", base, longPart.c_str()) == -1 && Error() == 63); + Require(renameat_nid_postfix(base, ".", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "..", base, "renamed.txt") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, ".") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "made.bin", base, "..") == -1 && Error() == 22); + Require(renameat_nid_postfix(base, "missing.bin", base, "renamed.txt") == -1 && Error() == 2); + Require(renameat_nid_postfix(base, "made.bin", base, "renamed.bin") == 0); + Require(!std::filesystem::exists(root / "made.bin")); + Require(std::filesystem::is_regular_file(root / "renamed.bin")); + Require(renameat_nid_postfix(base, "renamed.bin", base, "renamed.bin") == 0); + const auto subPath = root / "sub_dir"; + std::filesystem::create_directory(subPath); + Require(renameat_nid_postfix(base, "renamed.bin", base, "sub_dir") == -1 && Error() == 21); + Require(renameat_nid_postfix(base, "sub_dir", base, "renamed.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(base, "sub_dir", base, "sub_renamed") == 0); + Require(!std::filesystem::exists(subPath)); + Require(std::filesystem::is_directory(root / "sub_renamed")); + const int subFd = sceKernelOpen((root / "sub_renamed").string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(subFd >= 0); + Require(renameat_nid_postfix(base, "renamed.bin", subFd, "nested.bin") == 0); + Require(!std::filesystem::exists(root / "renamed.bin")); + Require(std::filesystem::is_regular_file(root / "sub_renamed" / "nested.bin")); + Require(renameat_nid_postfix(base, "sub_renamed", subFd, "child") == -1 && Error() == 22); + const auto otherDir = root / "other_dir"; + std::filesystem::create_directory(otherDir); + { std::ofstream stream(otherDir / "item.txt"); } + Require(renameat_nid_postfix(base, "sub_renamed", base, "other_dir") == -1 && Error() == 66); + Require(renameat_nid_postfix(subFd, "nested.bin/", base, "back.bin") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "nonexistent/") == -1 && Error() == 20); + Require(renameat_nid_postfix(subFd, "nested.bin", base, "made.bin") == 0); + Require(std::filesystem::is_regular_file(root / "made.bin")); + Require(sceKernelClose(subFd) == 0); + Require(renameat_nid_postfix(-100, (root / "made.bin").string().c_str(), -100, (root / "final.bin").string().c_str()) == 0); + Require(std::filesystem::is_regular_file(root / "final.bin")); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x0200) == 0); + Require(fchmodat_nid_postfix(-100, (root / "file.txt").string().c_str(), 0644, 0) == 0); +#ifndef _WIN32 + Require((std::filesystem::status(root / "file.txt").permissions() & std::filesystem::perms::all) == + (std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read)); +#endif + Require(fchmodat_nid_postfix(base, "missing.bin", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, nullptr, 0600, 0) == -1 && Error() == 14); + Require(fchmodat_nid_postfix(base, "", 0600, 0) == -1 && Error() == 2); + Require(fchmodat_nid_postfix(base, "file.txt", 0600, 0x4000) == -1 && Error() == 22); + Require(fchmodat_nid_postfix(-1, "file.txt", 0600, 0) == -1 && Error() == 9); +#ifndef _WIN32 + Require(symlink_nid_postfix("loop", (root / "loop").string().c_str()) == 0); + Require(fchmodat_nid_postfix(base, "loop/x", 0600, 0) == -1 && Error() == 62); + Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); +#endif Require(sceKernelClose(base) == 0); std::filesystem::remove_all(root); return 0; diff --git a/core/libs/tests/GuestKill.cpp b/core/libs/tests/GuestKill.cpp index 4a2d755475..7a1f380d3c 100644 --- a/core/libs/tests/GuestKill.cpp +++ b/core/libs/tests/GuestKill.cpp @@ -7,6 +7,7 @@ using Handler = void (APS5_VABI *)(int); extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI killpg_nid_postfix(int, int); int APS5_VABI getpid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } @@ -17,18 +18,33 @@ static void Fails(int pid, int guest, int error) { *__error_nid_postfix() = 0; Require(kill_nid_postfix(pid, guest) == -1 && *__error_nid_postfix() == error); } +static void KillpgFails(int pgrp, int guest, int error) { + *__error_nid_postfix() = 0; + Require(killpg_nid_postfix(pgrp, guest) == -1 && *__error_nid_postfix() == error); +} int main() { const int self = getpid_nid_postfix(); const auto invalid = reinterpret_cast(static_cast(-1)); const auto ignore = reinterpret_cast(std::uintptr_t{1}); for (int target : {self, 0, -self}) Require(kill_nid_postfix(target, 0) == 0); + for (int target : {self, 0}) Require(killpg_nid_postfix(target, 0) == 0); for (int target : {self + 1, -1, -(self + 1)}) { Fails(target, 0, 3); Fails(target, 15, 3); } + for (int target : {self + 1, 1000}) { + KillpgFails(target, 0, 3); + KillpgFails(target, 15, 3); + } + for (int target : {-1, -self}) { + KillpgFails(target, 0, 22); + KillpgFails(target, 15, 22); + } for (int guest : {-1, 129}) { Fails(self, guest, 22); Fails(self + 1, guest, 22); + KillpgFails(self, guest, 22); + KillpgFails(self + 1, guest, 22); } Require(signal_nid_postfix(15, Callback) != invalid); for (int target : {self, 0, -self}) { @@ -36,11 +52,19 @@ int main() { Require(kill_nid_postfix(target, 0) == 0 && received == 0); Require(kill_nid_postfix(target, 15) == 0 && received == 15); } + for (int target : {self, 0}) { + received = 0; + Require(killpg_nid_postfix(target, 0) == 0 && received == 0); + Require(killpg_nid_postfix(target, 15) == 0 && received == 15); + } received = 0; Fails(self + 1, 15, 3); + KillpgFails(self + 1, 15, 3); Require(received == 0); Require(signal_nid_postfix(15, ignore) == Callback); Require(kill_nid_postfix(self, 15) == 0 && received == 0); + Require(killpg_nid_postfix(self, 15) == 0 && received == 0); Require(signal_nid_postfix(15, nullptr) == ignore); Fails(self, 100, 22); + KillpgFails(self, 100, 22); } diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp new file mode 100644 index 0000000000..78c5bd5425 --- /dev/null +++ b/core/libs/tests/GuestKqueue.cpp @@ -0,0 +1,428 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI kqueue_nid_postfix(); +int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int, const KernelTimespec*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); +int APS5_VABI rmdir_nid_postfix(const char*); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelAddFileEvent(KernelEqueue eq, int fd, int watch, void* udata); +int APS5_VABI sceKernelDeleteFileEvent(KernelEqueue eq, int fd); +int* APS5_VABI __error_nid_postfix(); +} + +static constexpr int GuestEnoent = 2; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEinval = 22; +static constexpr int Unix = 1; +static constexpr int Stream = 1; +static constexpr int FirstSocket = 0x10000000; +static constexpr std::int16_t EvfiltRead = -1; +static constexpr std::int16_t EvfiltWrite = -2; +static constexpr std::int16_t EvfiltVnode = -4; +static constexpr std::int16_t EvfiltProc = -5; +static constexpr std::int16_t EvfiltTimer = -7; +static constexpr std::int16_t EvfiltUser = -11; +static constexpr std::uint16_t EvAdd = 0x0001; +static constexpr std::uint16_t EvDelete = 0x0002; +static constexpr std::uint16_t EvEnable = 0x0004; +static constexpr std::uint16_t EvDisable = 0x0008; +static constexpr std::uint16_t EvOneshot = 0x0010; +static constexpr std::uint16_t EvClear = 0x0020; +static constexpr std::uint16_t EvReceipt = 0x0040; +static constexpr std::uint16_t EvError = 0x4000; +static constexpr std::uint32_t NoteTrigger = 0x01000000; +static constexpr std::uint32_t NoteFfcopy = 0xc0000000; +static constexpr std::uint32_t NoteFfor = 0x80000000; +static constexpr std::uint32_t NoteUseconds = 0x0004; +static constexpr std::uint32_t NoteDelete = 0x0001; +static constexpr std::uint32_t NoteWrite = 0x0002; +static constexpr std::uint32_t NoteExtend = 0x0004; +static constexpr std::uint32_t NoteAttrib = 0x0008; +static constexpr std::uint32_t NoteLink = 0x0010; +static constexpr std::uint32_t NoteRename = 0x0020; +static constexpr std::uint32_t NoteRevoke = 0x0040; +static constexpr int ReadWrite = 0x0002; +static constexpr int Create = 0x0200; +static constexpr int Directory = 0x00020000; +static constexpr int SceOk = 0; +static constexpr int SceEnoent = static_cast(0x80020002); +static constexpr int SceEbadf = static_cast(0x80020009); +static constexpr int SceEinval = static_cast(0x80020016); + +static void Require(bool value) { if (!value) std::abort(); } + +static KernelEvent Change(std::uintptr_t ident, std::int16_t filter, std::uint16_t flags, std::uint32_t fflags = 0, std::intptr_t data = 0, void* udata = nullptr) { + KernelEvent change{}; + change.ident = ident; + change.filter = filter; + change.flags = flags; + change.fflags = fflags; + change.data = data; + change.udata = udata; + return change; +} + +static int Apply(int kq, const KernelEvent& change) { + return kevent_nid_postfix(kq, &change, 1, nullptr, 0, nullptr); +} + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +static int Poll(int kq, KernelEvent* events, int count) { + const KernelTimespec zero{0, 0}; + return kevent_nid_postfix(kq, nullptr, 0, events, count, &zero); +} + +static int Wait(int kq, KernelEvent* events, int count) { + const KernelTimespec second{1, 0}; + return kevent_nid_postfix(kq, nullptr, 0, events, count, &second); +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void CreateAndClose() { + const int kq = kqueue_nid_postfix(); + Require(kq >= 0 && kq < FirstSocket); + const int other = kqueue_nid_postfix(); + Require(other >= 0 && other != kq); + KernelEvent event{}; + Require(Poll(kq, &event, 1) == 0); + Require(Throws([kq] { dup_nid_postfix(kq); })); + Require(close_nid_postfix(kq) == 0); + RequireFailure(Poll(kq, &event, 1), GuestEbadf); + Require(close_nid_postfix(other) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + RequireFailure(Poll(pipe[0], &event, 1), GuestEbadf); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); +} + +static void Timers() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(1, EvfiltTimer, EvAdd, 0, 10, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == 1 && events[0].filter == EvfiltTimer && events[0].data >= 1 && events[0].udata == &marker); + Require((events[0].flags & EvClear) != 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == 1); + Require(Apply(kq, Change(1, EvfiltTimer, EvDelete)) == 0); + RequireFailure(Apply(kq, Change(1, EvfiltTimer, EvDelete)), GuestEnoent); + + Require(Apply(kq, Change(2, EvfiltTimer, EvAdd | EvOneshot, NoteUseconds, 0)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == 2 && events[0].data == 1); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(2, EvfiltTimer, EvDelete)), GuestEnoent); + + RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, 0, -1)), GuestEinval); + RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, 0x10, 5)), GuestEinval); + RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, NoteUseconds | 0x1, 5)), GuestEinval); + Require(Apply(kq, Change(4, EvfiltTimer, EvAdd, 0, 100000)) == 0); + Require(Throws([kq] { Apply(kq, Change(4, EvfiltTimer, EvAdd, 0, 5)); })); + Require(close_nid_postfix(kq) == 0); +} + +static void UserEvents() { + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(5, EvfiltUser, EvAdd | EvClear, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + std::thread trigger([kq, &marker] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(Apply(kq, Change(5, EvfiltUser, 0, NoteTrigger | NoteFfcopy | 0x123, 77, &marker)) == 0); + }); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, nullptr) == 1); + trigger.join(); + Require(events[0].ident == 5 && events[0].filter == EvfiltUser && events[0].fflags == 0x123 && events[0].data == 77); + Require(events[0].udata == &marker && events[0].flags == EvClear); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(5, EvfiltUser, 0, NoteTrigger | NoteFfor | 0x400, 78)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == 0x523 && events[0].data == 78); + + Require(Apply(kq, Change(6, EvfiltUser, EvAdd)) == 0); + Require(Apply(kq, Change(6, EvfiltUser, 0, NoteTrigger)) == 0); + for (int i = 0; i < 2; ++i) { + Require(Poll(kq, events, 4) == 1 && events[0].ident == 6); + } + Require(Apply(kq, Change(6, EvfiltUser, EvClear)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(6, EvfiltUser, EvDelete)) == 0); + + KernelEvent receipt[2]{}; + const KernelEvent change = Change(7, EvfiltUser, EvAdd | EvReceipt, 0x55, 9, &marker); + Require(kevent_nid_postfix(kq, &change, 1, receipt, 2, nullptr) == 1); + Require(receipt[0].ident == 7 && receipt[0].flags == EvError && receipt[0].data == 0 && receipt[0].fflags == 0); + Require(Apply(kq, Change(7, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == 0x55 && events[0].data == 0 && events[0].flags == EvReceipt); + Require(close_nid_postfix(kq) == 0); +} + +static void SocketPair() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(static_cast(pair[0]), EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "abc", 3, 0) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == static_cast(pair[0]) && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker); + Require(Poll(kq, events, 4) == 1); + char buffer[4]{}; + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + RequireFailure(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)), GuestEnoent); + Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltRead, EvAdd | EvClear)); })); + Require(close_nid_postfix(pair[0]) == 0); + RequireFailure(Apply(kq, Change(static_cast(pair[0]), EvfiltRead, EvDelete)), GuestEbadf); + Require(close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static void Errors() { + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const KernelEvent changes[3] = { + Change(FirstSocket + 4000, EvfiltRead, EvAdd), + Change(8, EvfiltUser, EvDelete), + Change(9, EvfiltUser, EvAdd | EvEnable | EvDisable), + }; + Require(kevent_nid_postfix(kq, changes, 3, events, 4, nullptr) == 3); + Require(events[0].flags == EvError && events[0].data == GuestEbadf && events[0].ident == FirstSocket + 4000); + Require(events[1].flags == EvError && events[1].data == GuestEnoent); + Require(events[2].flags == EvError && events[2].data == GuestEinval); + RequireFailure(kevent_nid_postfix(kq, changes, 3, events, 1, nullptr), GuestEnoent); + Require(events[0].flags == EvError && events[0].data == GuestEbadf); + RequireFailure(kevent_nid_postfix(kq, changes, 3, nullptr, 0, nullptr), GuestEbadf); + RequireFailure(Apply(kq, Change(static_cast(kq), EvfiltRead, EvAdd)), GuestEinval); + RequireFailure(Apply(kq, Change(1, 1, EvAdd)), GuestEinval); + Require(Throws([kq] { Apply(kq, Change(1, EvfiltProc, EvAdd)); })); + Require(Throws([kq] { Apply(kq, Change(1, EvfiltUser, EvAdd | EvDisable)); })); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd)); })); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const KernelEvent skipped = Change(1, 0, EvAdd); + const KernelTimespec zero{0, 0}; + Require(kevent_nid_postfix(kq, &skipped, 1, events, 4, &zero) == 0); + const KernelTimespec invalid{0, 1000000000}; + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, 4, &invalid), GuestEinval); + const KernelTimespec negative{-1, 0}; + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, 4, &negative), GuestEinval); + Require(close_nid_postfix(kq) == 0); +} + +static std::string TemporaryName(const char* stem) { + return std::string("anyps5-kqueue-") + stem + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); +} + +static void FileWatch() { + const auto path = TemporaryName("file"); + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, 0x25, NoteDelete | NoteWrite | NoteExtend | NoteLink | NoteRename, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + Require(Wait(kq, events, 4) == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && events[0].flags == EvClear && events[0].udata == &marker); + Require(events[0].fflags == (NoteWrite | NoteExtend) && events[0].data == 0); + Require(Poll(kq, events, 4) == 0); + Require(ftruncate_nid_postfix(file, 0) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteAttrib)) == 0); + Require(write_nid_postfix(file, "abc", 3) == 3); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(ftruncate_nid_postfix(file, 0) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].fflags == NoteAttrib && events[0].udata == nullptr); + Require(Throws([kq, ident] { Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteRevoke)); })); + Require(Apply(kq, Change(ident, EvfiltVnode, EvDelete)) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEnoent); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "file") == SceOk); + Require(sceKernelAddFileEvent(eq, file, static_cast(NoteWrite), &marker) == SceOk); + Require(write_nid_postfix(file, "defg", 4) == 4); + int count = 0; + const KernelUseconds second = 1000000; + Require(sceKernelWaitEqueue(eq, events, 4, &count, &second) == SceOk && count == 1); + Require(events[0].ident == ident && events[0].filter == EvfiltVnode && events[0].fflags == (NoteWrite | NoteExtend) && events[0].udata == &marker); + Require(sceKernelDeleteFileEvent(eq, file) == SceOk); + Require(sceKernelDeleteFileEvent(eq, file) == SceEnoent); + Require(sceKernelAddFileEvent(eq, FirstSocket + 4000, static_cast(NoteWrite), nullptr) == SceEbadf); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + Require(sceKernelAddFileEvent(eq, pair[0], static_cast(NoteWrite), nullptr) == SceEinval); + RequireFailure(Apply(kq, Change(static_cast(pair[0]), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + RequireFailure(Apply(kq, Change(static_cast(kq), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + RequireFailure(Apply(kq, Change(static_cast(pipe[0]), EvfiltVnode, EvAdd, NoteWrite)), GuestEinval); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd, NoteWrite)) == 0); + Require(sceKernelAddFileEvent(eq, file, static_cast(NoteWrite), nullptr) == SceOk); + Require(close_nid_postfix(file) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltVnode, EvDelete)), GuestEbadf); + Require(sceKernelDeleteFileEvent(eq, file) == SceEbadf); + Require(sceKernelDeleteEqueue(eq) == SceOk); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove(path)); +} + +#ifndef _WIN32 +static void RenameAndDeleteWatchedFile() { + const auto path = TemporaryName("moved"); + const auto renamed = path + "-renamed"; + const int file = open_nid_postfix(path.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(file); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteDelete | NoteRename)) == 0); + Require(sceKernelRename(path.c_str(), renamed.c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteRename); + Require(unlink_nid_postfix(renamed.c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteDelete); + Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(kq) == 0); +} + +static std::uint32_t NotesThrough(int kq, std::uintptr_t ident, std::uint32_t last) { + std::uint32_t notes = 0; + KernelEvent events[4]{}; + while ((notes & last) == 0) { + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident); + notes |= events[0].fflags; + } + return notes; +} + +static void MoveDirectoryEntries() { + const auto path = TemporaryName("moves"); + const auto outside = path + "-outside"; + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0 && mkdir_nid_postfix(outside.c_str(), 0755) == 0); + const int child = open_nid_postfix((path + "/first.bin").c_str(), ReadWrite | Create, 0644); + Require(child >= 0 && close_nid_postfix(child) == 0); + const int directory = open_nid_postfix(path.c_str(), Directory, 0); + Require(directory >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(directory); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite | NoteExtend | NoteLink)) == 0); + for (int i = 0; i < 64; ++i) { + Require(sceKernelRename((path + "/first.bin").c_str(), (path + "/second.bin").c_str()) == SceOk); + Require(sceKernelRename((path + "/second.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + } + Require(mkdir_nid_postfix((path + "/sub").c_str(), 0755) == 0); + Require(NotesThrough(kq, ident, NoteLink) == (NoteWrite | NoteLink)); + Require(sceKernelRename((path + "/first.bin").c_str(), (outside + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((outside + "/first.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((path + "/sub").c_str(), (outside + "/sub").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend | NoteLink)); + Require(close_nid_postfix(directory) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 2 && std::filesystem::remove_all(outside) == 2); +} +#endif + +static void DirectoryWatch() { + const auto path = TemporaryName("directory"); + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0); + const int directory = open_nid_postfix(path.c_str(), Directory, 0); + Require(directory >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(directory); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite | NoteLink | NoteDelete | NoteRename)) == 0); + Require(Poll(kq, events, 4) == 0); + const int child = open_nid_postfix((path + "/child.bin").c_str(), ReadWrite | Create, 0644); + Require(child >= 0 && close_nid_postfix(child) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteWrite); + Require(mkdir_nid_postfix((path + "/sub").c_str(), 0755) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].fflags == (NoteWrite | NoteLink)); + Require(sceKernelRename((path + "/child.bin").c_str(), (path + "/renamed.bin").c_str()) == SceOk); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == NoteWrite); + Require(unlink_nid_postfix((path + "/renamed.bin").c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == NoteWrite); + Require(rmdir_nid_postfix((path + "/sub").c_str()) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Poll(kq, events, 4) == 1 && events[0].fflags == (NoteWrite | NoteLink)); + Require(close_nid_postfix(directory) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 1); +} + +static void Timeout() { + const int kq = kqueue_nid_postfix(); + KernelEvent event{}; + const KernelTimespec timeout{0, 30000000}; + const auto start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(kq, nullptr, 0, &event, 1, &timeout) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(29)); + Require(kevent_nid_postfix(kq, nullptr, 0, &event, 0, nullptr) == 0); + Require(close_nid_postfix(kq) == 0); +} + +int main() { + CreateAndClose(); + Timers(); + UserEvents(); + SocketPair(); + Errors(); + FileWatch(); +#ifndef _WIN32 + RenameAndDeleteWatchedFile(); + MoveDirectoryEntries(); +#endif + DirectoryWatch(); + Timeout(); + return 0; +} diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp new file mode 100644 index 0000000000..07bb8719c3 --- /dev/null +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); +int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int timedout = 2; +constexpr int plain = 0x01; + +int main() { + void* mutex = nullptr; + void* condition = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, plain) == success); + Require(_Cnd_init_nid_postfix(&condition) == success); + + Require(_Mtx_lock_nid_postfix(&mutex) == success); + + const auto now = std::chrono::system_clock::now(); + const auto nowSec = std::chrono::duration_cast(now.time_since_epoch()).count(); + const auto nowNsec = std::chrono::duration_cast(now.time_since_epoch()).count() % 1000000000; + + Xtime pastTime{nowSec - 1, nowNsec}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &pastTime) == timedout); + + Xtime unnormalizedPastTime{nowSec - 2, nowNsec + 1000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedPastTime) == timedout); + + Xtime negativePastTime{nowSec, nowNsec - 2000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &negativePastTime) == timedout); + + const auto timeoutDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); + const auto timeoutSec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count(); + const auto timeoutNsec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count() % 1000000000; + const Xtime timeoutTime{timeoutSec, timeoutNsec}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &timeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= timeoutDeadline); + break; + } + Require(result == success); + } + + const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); + const auto unnormalizedSec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count(); + const auto unnormalizedNsec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count() % 1000000000; + const Xtime unnormalizedTimeoutTime{unnormalizedSec - 1, unnormalizedNsec + 1000000000}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedTimeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= unnormalizedDeadline); + break; + } + Require(result == success); + } + + Xtime futureTime{nowSec + 5, nowNsec}; + bool ready = false; + std::thread signaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + while (!ready) { + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &futureTime) == success); + } + signaler.join(); + + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, nullptr); }); + const Xtime overflowCarryTime{std::numeric_limits::max(), 1000000000}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowCarryTime); }); + const Xtime overflowSecTime{std::numeric_limits::max(), 0}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowSecTime); }); + _Cnd_destroy_nid_postfix(&condition); + _Mtx_destroy_nid_postfix(&mutex); +} diff --git a/core/libs/tests/GuestLibcMutexInitWithName.cpp b/core/libs/tests/GuestLibcMutexInitWithName.cpp new file mode 100644 index 0000000000..2501aef36b --- /dev/null +++ b/core/libs/tests/GuestLibcMutexInitWithName.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Mtx_init_with_name_nid_postfix(void** handle, int type, const char* name); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int busy = 3; +constexpr int plain = 0x01; + +int main() { + void* mutex = nullptr; + Require(_Mtx_init_with_name_nid_postfix(&mutex, plain, "GameMutex") == success); + Require(mutex != nullptr); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + Require(_Mtx_lock_nid_postfix(&mutex) == busy); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + _Mtx_destroy_nid_postfix(&mutex); + Require(mutex == nullptr); + + RequireThrows([] { + _Mtx_init_with_name_nid_postfix(nullptr, plain, "NullHandle"); + }); +} 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 5a8e85f5ff..34d56fcca0 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -33,6 +33,12 @@ float APS5_VABI sqrtf_nid_postfix(float); double APS5_VABI fabs_nid_postfix(double); int APS5_VABI abs_nid_postfix(int); double APS5_VABI trunc_nid_postfix(double); +double APS5_VABI ceil_nid_postfix(double); +double APS5_VABI floor_nid_postfix(double); +float APS5_VABI truncf_nid_postfix(float); +float APS5_VABI ceilf_nid_postfix(float); +float APS5_VABI floorf_nid_postfix(float); +double APS5_VABI nearbyint_nid_postfix(double); double APS5_VABI fma_nid_postfix(double, double, double); long double APS5_VABI acosl_nid_postfix(long double); long double APS5_VABI asinl_nid_postfix(long double); @@ -41,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); @@ -57,12 +64,17 @@ int APS5_VABI rand_nid_postfix(); void APS5_VABI srand_nid_postfix(unsigned int); int APS5_VABI rand_r_nid_postfix(unsigned int*); float APS5_VABI log10f_nid_postfix(float); +float APS5_VABI powf_nid_postfix(float, float); double APS5_VABI log1p_nid_postfix(double); float APS5_VABI log1pf_nid_postfix(float); double APS5_VABI expm1_nid_postfix(double); +float APS5_VABI expm1f_nid_postfix(float); +float APS5_VABI erff_nid_postfix(float); +double APS5_VABI lgamma_nid_postfix(double); float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); +double APS5_VABI __powidf2_nid_postfix(double, int); double APS5_VABI ldexp_nid_postfix(double, int); double APS5_VABI scalbn_nid_postfix(double, int); float APS5_VABI scalbnf_nid_postfix(float, int); @@ -80,14 +92,18 @@ float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); float APS5_VABI acoshf_nid_postfix(float); double APS5_VABI atanh_nid_postfix(double); +float APS5_VABI atanhf_nid_postfix(float); std::int64_t APS5_VABI lroundf_nid_postfix(float); std::int64_t APS5_VABI llround_nid_postfix(double); std::int64_t APS5_VABI llroundf_nid_postfix(float); +std::int64_t APS5_VABI llroundl_nid_postfix(long double); int APS5_VABI __isfinitef_nid_postfix(float); int APS5_VABI __isnormal_nid_postfix(double); int APS5_VABI __isnormalf_nid_postfix(float); int APS5_VABI __isinff_nid_postfix(float); int APS5_VABI __isinf_nid_postfix(double); +int APS5_VABI isnan_nid_postfix(double); +int APS5_VABI isinf_nid_postfix(double); std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); @@ -106,6 +122,7 @@ long double APS5_VABI log10l_nid_postfix(long double); long double APS5_VABI powl_nid_postfix(long double, long double); long double APS5_VABI ldexpl_nid_postfix(long double, int); long double APS5_VABI truncl_nid_postfix(long double); +long double APS5_VABI nearbyintl_nid_postfix(long double); double APS5_VABI logb_nid_postfix(double); double APS5_VABI remainder_nid_postfix(double, double); } @@ -131,6 +148,23 @@ static void CheckExactOperations() { Require(std::bit_cast(trunc_nid_postfix(-0.5)) == 0x8000000000000000ull); Require(trunc_nid_postfix(4503599627370497.) == 4503599627370497. && trunc_nid_postfix(-1e300) == -1e300); Require(trunc_nid_postfix(-INFINITY) == -INFINITY && std::isnan(trunc_nid_postfix(nan))); + Require(ceil_nid_postfix(2.2) == 3. && ceil_nid_postfix(-2.7) == -2. && ceil_nid_postfix(5.) == 5.); + Require(std::bit_cast(ceil_nid_postfix(-0.5)) == 0x8000000000000000ull); + Require(std::bit_cast(ceil_nid_postfix(-0.)) == 0x8000000000000000ull && ceil_nid_postfix(0x1p-1074) == 1.); + Require(ceil_nid_postfix(4503599627370495.5) == 4503599627370496. && ceil_nid_postfix(-1e300) == -1e300); + Require(ceil_nid_postfix(INFINITY) == INFINITY && ceil_nid_postfix(-INFINITY) == -INFINITY && std::isnan(ceil_nid_postfix(nan))); + Require(std::fesetround(FE_DOWNWARD) == 0); + const bool ceilIgnoresRounding = ceil_nid_postfix(2.2) == 3. && ceil_nid_postfix(-2.7) == -2.; + Require(std::fesetround(FE_TONEAREST) == 0 && ceilIgnoresRounding); + Require(floor_nid_postfix(2.7) == 2. && floor_nid_postfix(-2.2) == -3. && floor_nid_postfix(5.) == 5.); + Require(std::bit_cast(floor_nid_postfix(-0.5)) == 0xbff0000000000000ull); + Require(std::bit_cast(floor_nid_postfix(-0.)) == 0x8000000000000000ull && floor_nid_postfix(-0x1p-1074) == -1.); + Require(std::bit_cast(floor_nid_postfix(0.5)) == 0); + Require(floor_nid_postfix(4503599627370495.5) == 4503599627370495. && floor_nid_postfix(-1e300) == -1e300); + Require(floor_nid_postfix(INFINITY) == INFINITY && floor_nid_postfix(-INFINITY) == -INFINITY && std::isnan(floor_nid_postfix(nan))); + Require(std::fesetround(FE_UPWARD) == 0); + const bool floorIgnoresRounding = floor_nid_postfix(2.7) == 2. && floor_nid_postfix(-2.2) == -3.; + Require(std::fesetround(FE_TONEAREST) == 0 && floorIgnoresRounding); Require(fma_nid_postfix(2., 3., 4.) == 10.); volatile double tenth = 0.1; Require(fma_nid_postfix(tenth, 10., -1.) == 0x1p-54); @@ -138,6 +172,19 @@ static void CheckExactOperations() { Require(std::isnan(fma_nid_postfix(INFINITY, 0., 1.))); } +static void CheckNearbyint() { + std::feclearexcept(FE_ALL_EXCEPT); + const double zero = nearbyint_nid_postfix(0.5); + Require(zero == 0.0 && !std::signbit(zero)); + Require(nearbyint_nid_postfix(1.5) == 2.0 && nearbyint_nid_postfix(2.5) == 2.0); + Require(nearbyint_nid_postfix(2.7) == 3.0 && nearbyint_nid_postfix(-2.7) == -3.0); + Require(std::bit_cast(nearbyint_nid_postfix(-0.5)) == 0x8000000000000000ull); + Require(nearbyint_nid_postfix(0x1p53) == 0x1p53); + Require(nearbyint_nid_postfix(INFINITY) == INFINITY && nearbyint_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(nearbyint_nid_postfix(NAN))); + Require(std::fetestexcept(FE_INEXACT) == 0); +} + static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_t signExponent) { unsigned char bytes[sizeof(long double)]{}; std::memcpy(bytes, &value, sizeof(value)); @@ -148,6 +195,45 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_ return actualMantissa == mantissa && actualSignExponent == signExponent; } +static void CheckNanConversions() { + const struct { const char* text; std::size_t consumed; bool negative; } cases[] = { + {"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false}, + {"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false}, + }; + bool correct = true; + for (const auto& test : cases) { + const char* expectedEnd = test.text + test.consumed; + const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull; + char* end = nullptr; + const auto value = std::bit_cast(strtod_nid_postfix(test.text, &end)); + if (value != expectedDouble || end != expectedEnd) { + std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast(value)); + correct = false; + } + const auto ascii = std::bit_cast(atof_nid_postfix(test.text)); + if (ascii != expectedDouble) { + std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast(ascii)); + correct = false; + } + const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); + if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) { + std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); + correct = false; + } + const long double extended = strtold_nid_postfix(test.text, &end); + if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) { + std::uint64_t significand = 0; + std::uint16_t signExponent = 0; + std::memcpy(&significand, &extended, sizeof(significand)); + std::memcpy(&signExponent, reinterpret_cast(&extended) + sizeof(significand), sizeof(signExponent)); + std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast(signExponent), + static_cast(significand)); + correct = false; + } + } + Require(correct); +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -170,6 +256,20 @@ static void CheckExp2Errno() { Require(std::fetestexcept(FE_OVERFLOW | FE_INEXACT) == (FE_OVERFLOW | FE_INEXACT)); } +static void CheckPowfSpecialValue() { + Require(std::bit_cast(powf_nid_postfix(1.0f, std::bit_cast(0x7f800001u))) == 0x3f800000u); +} + +static void CheckAcosfErrno() { + errno = 0; + Require(std::bit_cast(acosf_nid_postfix(2.0f)) == 0xffc00000u); + Require(errno == 0); + Require(std::bit_cast(acosf_nid_postfix(-2.0f)) == 0xffc00000u); + Require(errno == 0); + Require(std::bit_cast(acosf_nid_postfix(0.5f)) == 0x3f860a92u); + Require(errno == 0); +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -192,6 +292,19 @@ static void CheckLongDouble() { Require(std::isnan(frexpl_nid_postfix(NAN, &exponent)) && exponent == 77); } +static void CheckNearbyintl() { + std::feclearexcept(FE_ALL_EXCEPT); + const long double zero = nearbyintl_nid_postfix(0.5L); + Require(zero == 0.0L && !std::signbit(zero)); + Require(nearbyintl_nid_postfix(1.5L) == 2.0L && nearbyintl_nid_postfix(2.5L) == 2.0L); + Require(nearbyintl_nid_postfix(2.7L) == 3.0L && nearbyintl_nid_postfix(-2.7L) == -3.0L); + Require(ExtendedBits(nearbyintl_nid_postfix(-0.5L), 0, 0x8000)); + Require(nearbyintl_nid_postfix(0x1p200L) == 0x1p200L); + Require(nearbyintl_nid_postfix(INFINITY) == INFINITY && nearbyintl_nid_postfix(-INFINITY) == -INFINITY); + Require(std::isnan(nearbyintl_nid_postfix(NAN))); + Require(std::fetestexcept(FE_INEXACT) == 0); +} + static void CheckFmodErrno() { errno = 0; (void)fmod_nid_postfix(1.0, 0.0); @@ -491,6 +604,39 @@ static void CheckSinhf() { *__error_nid_postfix() = 0; } +static void CheckAtanhf() { + const struct { std::uint32_t input; std::uint32_t output; } cases[] = { + {0x00000000u, 0x00000000u}, {0x80000000u, 0x80000000u}, {0x00000001u, 0x00000001u}, {0x0da24260u, 0x0da24260u}, + {0x31000000u, 0x31000000u}, {0xb1000000u, 0xb1000000u}, {0x31800000u, 0x31800000u}, {0x35800000u, 0x35800000u}, + {0x3dcccccdu, 0x3dcd7c9eu}, {0x3e800000u, 0x3e82c578u}, {0xbe800000u, 0xbe82c578u}, {0x3eaaaaaau, 0x3eb17217u}, + {0x3eaaaaabu, 0x3eb17218u}, {0x3effffffu, 0x3f0c9f53u}, {0x3f000000u, 0x3f0c9f54u}, {0xbf000000u, 0xbf0c9f54u}, + {0x3f199999u, 0x3f317217u}, {0x3f19999au, 0x3f317218u}, {0x3f400000u, 0x3f791396u}, {0xbf400000u, 0xbf791396u}, + {0x3f666666u, 0x3fbc71afu}, {0x3f7fffffu, 0x410aa123u}, {0xbf7fffffu, 0xc10aa123u}, + }; + for (const auto& test : cases) { + *__error_nid_postfix() = 12345; + const float result = atanhf_nid_postfix(std::bit_cast(test.input)); + if (std::bit_cast(result) != test.output || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest atanhf failed for %08x\n", test.input); + std::abort(); + } + } + for (const std::uint32_t input : {0x3f800000u, 0xbf800000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + const float result = atanhf_nid_postfix(std::bit_cast(input)); + Require(std::bit_cast(result) == (input | 0x7f800000u) && *__error_nid_postfix() == ERANGE); + Require(std::fetestexcept(FE_DIVBYZERO) != 0); + } + for (const std::uint32_t input : {0x3f800001u, 0xbf800001u, 0x3fc00000u, 0xbfc00000u, 0x7f7fffffu, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0xffc00000u}) { + std::feclearexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 0; + Require(std::isnan(atanhf_nid_postfix(std::bit_cast(input))) && *__error_nid_postfix() == EDOM); + Require(std::fetestexcept(FE_INVALID) != 0); + } + *__error_nid_postfix() = 0; +} + static void CheckFloatClassification() { Require(_FInf_nid_postfix.bits[0] == 0x7f800000u && _FNan_nid_postfix.bits[0] == 0x7fc00000u); for (int word = 1; word < 4; ++word) Require(_FInf_nid_postfix.bits[word] == 0 && _FNan_nid_postfix.bits[word] == 0); @@ -531,6 +677,44 @@ static void CheckMissingClassificationExports() { Require(__signbitl_nid_postfix(negativeNan) == 1); } +static void CheckFloatMathExports() { + Require(ceilf_nid_postfix(2.25f) == 3.f && ceilf_nid_postfix(-2.25f) == -2.f); + Require(floorf_nid_postfix(2.75f) == 2.f && floorf_nid_postfix(-2.25f) == -3.f); + Require(truncf_nid_postfix(2.75f) == 2.f && truncf_nid_postfix(-2.75f) == -2.f); + Require(ceilf_nid_postfix(2.f) == 2.f && floorf_nid_postfix(-2.f) == -2.f && truncf_nid_postfix(2.f) == 2.f); + Require(std::bit_cast(ceilf_nid_postfix(-0.f)) == 0x80000000u); + Require(std::bit_cast(floorf_nid_postfix(-0.f)) == 0x80000000u); + Require(std::bit_cast(truncf_nid_postfix(-0.f)) == 0x80000000u); + Require(ceilf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(floorf_nid_postfix(-std::numeric_limits::infinity()) == -std::numeric_limits::infinity()); + Require(std::isnan(ceilf_nid_postfix(std::numeric_limits::quiet_NaN()))); + + const double nan = std::numeric_limits::quiet_NaN(); + const double infinity = std::numeric_limits::infinity(); + Require(isnan_nid_postfix(nan) == 1 && isnan_nid_postfix(infinity) == 0 && isnan_nid_postfix(0.0) == 0); + Require(isnan_nid_postfix(-nan) == 1 && isnan_nid_postfix(-1.0) == 0); + Require(isinf_nid_postfix(infinity) == 1 && isinf_nid_postfix(-infinity) == 1); + 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); @@ -597,6 +781,7 @@ int main() { CheckFloatClassification(); CheckNoConversionIsPositiveZero(); CheckMissingClassificationExports(); + CheckFloatMathExports(); CheckIntegerConversions(); CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); @@ -609,6 +794,7 @@ int main() { Require(std::isinf(strtof_nid_postfix("1e1000", nullptr))); Require(*__error_nid_postfix() == 34); Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!'); + CheckNanConversions(); Require(fmodf_nid_postfix(5.5f, 2.f) == 1.5f); Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f); Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f))); @@ -663,6 +849,15 @@ int main() { Require(expm1_nid_postfix(-std::numeric_limits::infinity()) == -1.); Require(std::abs(expm1_nid_postfix(1.) - 1.718281828459045) < 1e-15); Require(std::abs(expm1_nid_postfix(1e-10) - 1.00000000005e-10) < 1e-25); + Require(expm1f_nid_postfix(0.f) == 0.f && std::signbit(expm1f_nid_postfix(-0.f)) && expm1f_nid_postfix(-1000.f) == -1.f); + Require(std::abs(expm1f_nid_postfix(1.f) - 1.7182818f) < 1e-6f && std::isinf(expm1f_nid_postfix(std::numeric_limits::infinity()))); + Require(erff_nid_postfix(0.f) == 0.f && std::signbit(erff_nid_postfix(-0.f)) && erff_nid_postfix(10.f) == 1.f && erff_nid_postfix(-10.f) == -1.f); + Require(std::abs(erff_nid_postfix(0.5f) - 0.5204999f) < 1e-6f && std::isnan(erff_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(lgamma_nid_postfix(1.) == 0. && lgamma_nid_postfix(2.) == 0. && std::isinf(lgamma_nid_postfix(0.))); + Require(std::abs(lgamma_nid_postfix(0.5) - 0.5723649429247001) < 1e-15 && std::abs(lgamma_nid_postfix(10.) - 12.801827480081469) < 1e-13); + 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); 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()); @@ -673,7 +868,9 @@ int main() { CheckSquareRoot(); CheckExp2Errno(); CheckExactOperations(); + CheckNearbyint(); CheckLongDouble(); + CheckNearbyintl(); CheckAsinl(); Require(scalbn_nid_postfix(0.75, -2) == 0.1875); Require(scalbnf_nid_postfix(0.75f, 4) == 12.f); @@ -736,7 +933,9 @@ int main() { Require(negativeDenominator.quot == -3 && negativeDenominator.rem == 1); const auto minimum = div_nid_postfix(std::numeric_limits::min(), 10); Require(minimum.quot == -214748364 && minimum.rem == -8); + CheckPowfSpecialValue(); Require(abs_nid_postfix(0) == 0); + CheckAcosfErrno(); Require(abs_nid_postfix(42) == 42); Require(abs_nid_postfix(-42) == 42); Require(abs_nid_postfix(std::numeric_limits::max()) == std::numeric_limits::max()); @@ -756,8 +955,25 @@ int main() { Require(std::signbit(__powisf2_nid_postfix(-0.0f, 3))); Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); + Require(__powidf2_nid_postfix(2.0, 10) == 1024.0); + Require(__powidf2_nid_postfix(2.0, -10) == 0.0009765625); + Require(__powidf2_nid_postfix(-2.0, 3) == -8.0); + Require(__powidf2_nid_postfix(-2.0, -3) == -0.125); + Require(__powidf2_nid_postfix(-2.0, 4) == 16.0); + Require(__powidf2_nid_postfix(0.0, 0) == 1.0); + Require(__powidf2_nid_postfix(std::numeric_limits::quiet_NaN(), 0) == 1.0); + Require(std::isnan(__powidf2_nid_postfix(std::numeric_limits::quiet_NaN(), 1))); + Require(__powidf2_nid_postfix(-1.0, std::numeric_limits::min()) == 1.0); + Require(__powidf2_nid_postfix(-1.0, std::numeric_limits::max()) == -1.0); + Require(__powidf2_nid_postfix(2.0, std::numeric_limits::min()) == 0.0); + Require(std::isinf(__powidf2_nid_postfix(0.0, -1))); + Require(std::signbit(__powidf2_nid_postfix(-0.0, 3))); + Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); + Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); CheckSinhf(); + CheckAtanhf(); CheckHypotSpecialValue(); CheckFmodErrno(); + CheckDoubleClassification(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index fb33dafc1d..9a12142d84 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -42,12 +42,14 @@ extern "C" { void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI sceKernelMmap(void*, std::size_t, int, int, int, std::int64_t, void**); int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; int APS5_VABI madvise_nid_postfix(void*, std::size_t, int); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelMapNamedFlexibleMemory(void**, std::size_t, int, int, const char*); int APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void**, std::size_t, int, int, const char*); int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); +int APS5_VABI sceKernelInternalMemoryGetAvailableSize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); @@ -58,6 +60,7 @@ int APS5_VABI sceKernelMemoryPoolBatch(const KernelMemoryPoolBatchEntry*, int, i int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); +int APS5_VABI sceKernelGetPageTableStats(int*, int*, int*, int*); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); int APS5_VABI sceKernelEnableDmemAliasing(); @@ -113,6 +116,46 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelMunmap(again, length) == 0); } +static void CheckFlexibleMappingIgnoresFlag8000() { + constexpr std::size_t length = 0x10000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0x8000) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 1; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelMunmap(mapped, length) == 0); + void* again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x8090) == 0 && again == mapped); + Require(static_cast(again)[length - 1] == 0); + Require(sceKernelMunmap(again, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + bool rejected = false; + void* unknown = nullptr; + try { sceKernelMapFlexibleMemory(&unknown, length, 3, 0x20000); } catch (const std::exception&) { rejected = true; } + Require(rejected && unknown == nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +static void CheckPageTableStats() { + int cpuTotal = -1, cpuAvailable = -1, gpuTotal = -1, gpuAvailable = -1; + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, &gpuTotal, &gpuAvailable) == 0); + Require(cpuTotal > 0 && gpuTotal > 0); + Require(cpuAvailable >= 0 && cpuAvailable <= cpuTotal); + Require(gpuAvailable >= 0 && gpuAvailable <= gpuTotal); + Require(sceKernelGetPageTableStats(nullptr, &cpuAvailable, &gpuTotal, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, nullptr, &gpuTotal, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, nullptr, &gpuAvailable) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetPageTableStats(&cpuTotal, &cpuAvailable, &gpuTotal, nullptr) == SCE_KERNEL_ERROR_EFAULT); +} + +static void CheckInternalMemoryAvailableSize() { + std::size_t available = 1; + Require(sceKernelInternalMemoryGetAvailableSize(&available) == 0 && available == 0); + Require(sceKernelInternalMemoryGetAvailableSize(nullptr) == static_cast(0x80020016u)); +} + static void CheckNamedAndHintedMappings() { constexpr std::size_t length = 0x10000; void* first = nullptr; @@ -229,6 +272,57 @@ static void CheckBatchMapStopsAtInvalidEntry() { Require(sceKernelMunmap(entries[0].start, page) == 0); } +static void CheckVirtualQueryFindsNeighbours() { + constexpr std::size_t page = 0x4000; + void* low = nullptr; + void* high = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&low, page * 4, 3, 0, "neighbours") == 0 && low != nullptr); + Require(sceKernelMapFlexibleMemory(&high, page, 3, 0) == 0 && high != nullptr); + auto* lowBytes = static_cast(low); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(lowBytes + page, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) && info.end == reinterpret_cast(low) + page * 4); + Require(sceKernelMprotect(lowBytes + page, page, 1) == 0); + Require(sceKernelVirtualQuery(lowBytes + page, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page && info.end == reinterpret_cast(low) + page * 2 && info.protection == 1); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 0, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page * 2 && info.end == reinterpret_cast(low) + page * 4 && info.protection == 3); + Require(sceKernelMunmap(lowBytes + page * 2, page) == 0); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 0, &info, sizeof(info)) != 0); + Require(sceKernelVirtualQuery(lowBytes + page * 2, 1, &info, sizeof(info)) == 0); + Require(info.start == reinterpret_cast(low) + page * 3 && info.end == reinterpret_cast(low) + page * 4); + Require(sceKernelVirtualQuery(lowBytes + page * 3, 0, &info, sizeof(info)) == 0 && info.start == reinterpret_cast(low) + page * 3); + Require(sceKernelMunmap(low, page * 2) == 0); + Require(sceKernelMunmap(lowBytes + page * 3, page) == 0); + Require(sceKernelMunmap(high, page) == 0); +} + +static void CheckReleaseFindsEveryDirectMapping() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t pieces = 48; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * pieces, 0, 0, &phys) == 0); + void* mapped[pieces] = {}; + for (std::size_t index = 0; index < pieces; ++index) { + const auto piece = pieces - 1 - index; + Require(sceKernelMapDirectMemory(&mapped[piece], page, 3, 0, phys + static_cast(piece * page), 0) == 0); + static_cast(mapped[piece])[0] = static_cast(piece); + } + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page * 2, 3, 0, phys + static_cast(page * 10), 0) == 0); + Require(static_cast(alias)[0] == 10 && static_cast(alias)[page] == 11); + Require(sceKernelReleaseDirectMemory(phys + static_cast(page * 10), page * 3) == 0); + VirtualQueryInfo info{}; + for (std::size_t index = 0; index < pieces; ++index) { + const bool released = index >= 10 && index < 13; + Require((sceKernelVirtualQuery(mapped[index], 0, &info, sizeof(info)) == 0) != released); + } + Require(sceKernelVirtualQuery(alias, 0, &info, sizeof(info)) != 0); + Require(sceKernelReleaseDirectMemory(phys, page * 10) == 0); + Require(sceKernelReleaseDirectMemory(phys + static_cast(page * 13), page * (pieces - 13)) == 0); + for (std::size_t index = 0; index < pieces; ++index) Require(sceKernelVirtualQuery(mapped[index], 0, &info, sizeof(info)) != 0); +} + static void CheckCheckedReleaseDirectMemory() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -997,6 +1091,81 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { Require(sceKernelMunmap(mapped, page * 2) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); } +#ifdef _WIN32 + +static void CheckAliasOfReorderedSharedPages() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + bytes[5] = 0x11; + bytes[page + 5] = 0x22; + void* low = mapped; + Require(sceKernelMapDirectMemory(&low, page, 3, 0x10, phys + page, 0) == 0); + void* high = static_cast(mapped) + page; + Require(sceKernelMapDirectMemory(&high, page, 3, 0x10, phys, 0) == 0); + Require(bytes[5] == 0x22 && bytes[page + 5] == 0x11); + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2)); + Require(alias != nullptr && alias[5] == 0x22 && alias[page + 5] == 0x11); + alias[6] = 0x33; + alias[page + 6] = 0x44; + Require(bytes[6] == 0x33 && bytes[page + 6] == 0x44); + GuestArena::GuestArenaUnmapAlias_nid_postfix(const_cast(alias)); + Require(bytes[5] == 0x22 && bytes[page + 5] == 0x11); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + +static void CheckAliasAcrossDirectAllocations() { + constexpr std::size_t page = 0x4000; + std::int64_t first = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &first) == 0); + std::int64_t second = 0; + Require(sceKernelAllocateDirectMemory(first + page * 2, first + page * 3, page, 0, 0, &second) == 0 && second == first + page * 2); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, first, 0) == 0); + auto* bytes = static_cast(mapped); + for (std::size_t index = 0; index < 3; ++index) bytes[page * index + 5] = static_cast(0x11 * (index + 1)); + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 3)); + Require(alias != nullptr); + MEMORY_BASIC_INFORMATION run{}; + Require(VirtualQuery(alias, &run, sizeof(run)) == sizeof(run) && run.RegionSize == page * 2); + Require(VirtualQuery(alias + page * 2, &run, sizeof(run)) == sizeof(run) && run.AllocationBase == alias + page * 2 && run.RegionSize == page); + auto* through = static_cast(alias); + for (std::size_t index = 0; index < 3; ++index) { + Require(through[page * index + 5] == 0x11 * (index + 1)); + through[page * index + 6] = static_cast(0x44 + index); + Require(bytes[page * index + 6] == 0x44 + index); + } + GuestArena::GuestArenaUnmapAlias_nid_postfix(alias); + for (std::size_t offset = 0; offset < page * 3; offset += page) { + MEMORY_BASIC_INFORMATION released{}; + Require(VirtualQuery(alias + offset, &released, sizeof(released)) == sizeof(released) && released.State == MEM_FREE); + } + Require(bytes[5] == 0x11 && bytes[page + 5] == 0x22 && bytes[page * 2 + 5] == 0x33); + Require(sceKernelMunmap(mapped, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(first, page * 3) == 0); +} + +static void CheckAliasRefusesPagesThatAreNotSharedViews() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0); + void* shared = reserved; + Require(sceKernelMapDirectMemory(&shared, page, 3, 0x10, phys, 0) == 0); + void* privatePage = static_cast(reserved) + page; + Require(sceKernelMapFlexibleMemory(&privatePage, page, 3, 0x10) == 0); + bool refused = false; + try { GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(reserved), page * 2); } catch (const std::runtime_error&) { refused = true; } + Require(refused); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} +#endif #if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) static std::size_t LockedKilobytes() { @@ -1172,20 +1341,20 @@ static void CheckSharedWriteTracking() { auto* right = static_cast(second); left[page + 5] = 21; Require(right[page + 5] == 21); - Require(collect(first, page * 3, false) == 4); - Require(collect(first, page * 3, false) == 4); - Require(collect(first, page * 3) == 4); - Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3, false) == 1); + Require(collect(first, page * 3, false) == 1); + Require(collect(first, page * 3) == 1); + Require(collect(second, page * 3) == 1); Require(collect(first, page * 3) == 0); right[page * 2] = 42; - Require(collect(first, page * 3) == 4); - Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 1); + Require(collect(second, page * 3) == 1); Require(collect(second, page * 3) == 0); Require(sceKernelMprotect(first, page * 3, 1) == 0); collect(first, page * 3); collect(second, page * 3); right[0] = 63; - Require(left[0] == 63 && collect(first, page * 3) == 4); + Require(left[0] == 63 && collect(first, page * 3) == 1); Require(sceKernelMprotect(first, page * 3, 3) == 0); collect(first, page * 3); left[0] = 84; @@ -1195,7 +1364,7 @@ static void CheckSharedWriteTracking() { collect(first, page * 3); collect(second, page * 3); static_cast(third)[0] = 105; - Require(collect(first, page * 3) == 4 && collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 1 && collect(second, page * 3) == 1); Require(sceKernelMunmap(third, page) == 0); Require(sceKernelMunmap(first, page * 3) == 0); Require(sceKernelMunmap(second, page * 3) == 0); @@ -1203,6 +1372,93 @@ static void CheckSharedWriteTracking() { #endif } +static void CheckSharedWritePages() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t small = 0x1000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + const auto collect = [&](std::size_t offset, std::size_t size) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapped) + offset, size, pages.data(), &count, true)); + Require(count == 0 || reinterpret_cast(pages[0]) >= reinterpret_cast(mapped) + offset); + return count; + }; + collect(0, page * 2); + Require(collect(0, page * 2) == 0); + static_cast(bytes)[small + 3] = 1; + Require(collect(small * 2, small * 2) == 0); + Require(collect(0, page * 2) == 1); + Require(collect(0, page * 2) == 0); + std::memset(bytes + small * 2 - 8, 7, 16); + Require(collect(0, page * 2) == 2); + Require(collect(0, page * 2) == 0); + const auto path = std::filesystem::path("anyps5-shared-pages-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + { + std::ofstream stream(path, std::ios::binary); + const std::vector seed(small, 5); + stream.write(seed.data(), static_cast(seed.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + Require(sceKernelPread(fd, bytes + page + small, small, 0) == static_cast(small)); + Require(sceKernelClose(fd) == 0); + Require(bytes[page + small] == 5 && bytes[page + small * 2 - 1] == 5); + Require(collect(page, small) == 0); + Require(collect(0, page * 2) == 1); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + std::filesystem::remove(path); +#endif +} + +static void CheckSharedWritesDuringHostWrite() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + constexpr std::size_t small = 0x1000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys, 0) == 0); + auto* left = static_cast(first); + auto* right = static_cast(second); + const auto collect = [](void* address) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), page, pages.data(), &count, true)); + std::vector offsets; + for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - reinterpret_cast(address)); + return offsets; + }; + const std::vector open{small, small * 2}; + collect(first); + collect(second); + Require(collect(first).empty() && collect(second).empty()); + left[small] = 1; + auto* target = const_cast(left + small * 2); + Require(GuestArena::GuestArenaBeginHostWrite_nid_postfix(target, small)); + Require(collect(first) == open); + Require(collect(second) == open); + left[small + 8] = 2; + Require(collect(first) == open); + Require(collect(second) == open); + GuestArena::GuestArenaEndHostWrite_nid_postfix(target, small); + Require(collect(first) == open && collect(second) == open); + Require(collect(first).empty() && collect(second).empty()); + left[small + 16] = 3; + Require(right[small + 16] == 3 && collect(second) == std::vector{small}); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +#endif +} + static void CheckFailedCollectKeepsWrites() { #ifdef _WIN32 constexpr std::size_t page = 0x4000; @@ -1230,7 +1486,7 @@ static void CheckFailedCollectKeepsWrites() { shared[8] = 2; Require(sceKernelMprotect(const_cast(shared + page), page, 0) == 0); Require(!collect(mapped, page * 2, count)); - Require(collect(mapped, page, count) && count == 4); + Require(collect(mapped, page, count) && count == 1); Require(sceKernelMunmap(mapped, page * 2) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); #endif @@ -1265,7 +1521,7 @@ static void CheckPinnedSharedPages() { Require(protection(page) == PAGE_READWRITE); bytes[page + 8] = 7; bytes[0] = 9; - Require(collect() == 8); + Require(collect() == 5); Require(collect() == 4); Require(protection(0) == PAGE_READONLY && protection(page) == PAGE_READWRITE); GuestArena::GuestArenaUnpinWritable_nid_postfix(const_cast(bytes + page), page); @@ -1273,7 +1529,7 @@ static void CheckPinnedSharedPages() { Require(collect() == 0); Require(protection(page) == PAGE_READONLY); bytes[page + 8] = 11; - Require(collect() == 4); + Require(collect() == 1); Require(sceKernelMunmap(mapped, page * 3) == 0); Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); #endif @@ -1780,6 +2036,18 @@ static void CheckVirtualQueryForNonReadableGuestRange() { Require(sceKernelMunmap(mapped, page) == 0); } +static void CheckSceKernelMmap() { + constexpr std::size_t page = 0x4000; + void* mapped = nullptr; + Require(sceKernelMmap(nullptr, page, 3, 0x1002, -1, 0, &mapped) == 0 && mapped != nullptr); + static_cast(mapped)[page - 1] = 7; + Require(static_cast(mapped)[page - 1] == 7); + Require(sceKernelMunmap(mapped, page) == 0); + void* untouched = &mapped; + Require(sceKernelMmap(nullptr, 0, 3, 0x1002, -1, 0, &untouched) == static_cast(0x80020016u)); + Require(untouched == &mapped); +} + static void CheckVirtualQueryPartialMunmap() { constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); @@ -1869,8 +2137,12 @@ int main() { CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); + CheckFlexibleMappingIgnoresFlag8000(); + CheckSceKernelMmap(); + CheckInternalMemoryAvailableSize(); CheckVirtualQueryPartialMunmap(); CheckMunmapOverUnmappedPages(); + CheckPageTableStats(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckGuestModuleUsesFlexibleMemory(); @@ -1902,6 +2174,9 @@ int main() { CheckFixedMappingsReachTheApplicationAreaEnd(); #ifdef _WIN32 CheckGuestModuleImageProtection(); + CheckAliasAcrossDirectAllocations(); + CheckAliasOfReorderedSharedPages(); + CheckAliasRefusesPagesThatAreNotSharedViews(); #endif CheckUnhintedKernelMappingsLandInTheWindow(); #if defined(__linux__) @@ -1911,6 +2186,8 @@ int main() { CheckArenaReusesAFreedRange(); CheckArenaHonoursHintsWithoutGoingBelowThem(); CheckSharedWriteTracking(); + CheckSharedWritePages(); + CheckSharedWritesDuringHostWrite(); CheckReadsIntoSharedWriteTracking(); CheckPinnedSharedPages(); CheckFailedCollectKeepsWrites(); @@ -1923,6 +2200,8 @@ int main() { CheckMappingCostFollowsTheMappingCount(); CheckDirectMemoryBackingNeedsNoFilesystem(); CheckDirectMemorySharedBacking(); + CheckVirtualQueryFindsNeighbours(); + CheckReleaseFindsEveryDirectMapping(); #endif constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); @@ -1966,7 +2245,9 @@ int main() { GuestAllocations::Mutation mutation; Require(mutation.Find(memory).writable); } - for (int advice = 0; advice <= 9; ++advice) Require(madvise_nid_postfix(memory, page * 3, advice) == 0); + for (int advice = 0; advice <= 10; ++advice) Require(madvise_nid_postfix(memory, page * 3, advice) == 0); + Require(madvise_nid_postfix(reinterpret_cast(0x800000000000), 1, 10) == 0); + Require(madvise_nid_postfix(memory, std::numeric_limits::max(), 10) == 0); Require(memory[0] == 42 && memory[page * 2] == 73); Require(madvise_nid_postfix(memory + 1, 0, 4) == 0); Require(madvise_nid_postfix(memory, page, 11) == -1 && *__error_nid_postfix() == 22); diff --git a/core/libs/tests/GuestMktime.cpp b/core/libs/tests/GuestMktime.cpp index 0a81573f9e..beaf273370 100644 --- a/core/libs/tests/GuestMktime.cpp +++ b/core/libs/tests/GuestMktime.cpp @@ -61,19 +61,6 @@ static void CheckFailure(Converter convert) { const GuestTm original = input; Require(convert(&input) == -1, "mktime accepted an unrepresentable calendar"); Require(Equal(input, original), "Failed mktime changed the guest calendar"); -#ifdef _WIN32 - input = {}; - input.tm_year = 69; - input.tm_mon = 11; - input.tm_mday = 31; - input.tm_hour = 23; - input.tm_min = 59; - input.tm_sec = 59; - input.tm_zone = "unchanged"; - const GuestTm beforeEpoch = input; - Require(convert(&input) == -1, "Windows mktime accepted a time before the epoch"); - Require(Equal(input, beforeEpoch), "Failed Windows mktime changed the guest calendar"); -#endif } static void CheckZone(const char* zone) { @@ -87,9 +74,6 @@ static void CheckZone(const char* zone) { constexpr std::array timers{-86401, -2, -1, 0, 1, 59, 60, 86400}; for (auto convert : converters) { for (const auto timer : timers) { -#ifdef _WIN32 - if (timer < 0) continue; -#endif CheckNormalization(convert, timer); } CheckFailure(convert); @@ -99,7 +83,5 @@ static void CheckZone(const char* zone) { int main() { CheckZone("UTC0"); CheckZone("UTC-2"); -#ifndef _WIN32 CheckZone("UTC5"); -#endif } diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 044cb68286..71ed127626 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX @@ -20,11 +21,27 @@ int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI mlock_nid_postfix(const void*, std::size_t); int APS5_VABI munlock_nid_postfix(const void*, std::size_t); +int APS5_VABI mlockall_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } +static void RequireLockAllInvalid(int flags) { + *__error_nid_postfix() = 0; + Require(mlockall_nid_postfix(flags) == -1 && *__error_nid_postfix() == 22); +} + +static void RequireLockAllUnsupported(int flags) { + bool rejected = false; + try { + mlockall_nid_postfix(flags); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} + #if defined(__linux__) static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); @@ -114,5 +131,13 @@ int main() { Require(mlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); *__error_nid_postfix() = 0; Require(munlock_nid_postfix(mapped, page) == -1 && *__error_nid_postfix() == 12); + + RequireLockAllInvalid(0); + RequireLockAllInvalid(4); + RequireLockAllInvalid(1 | 4); + RequireLockAllInvalid(-1); + RequireLockAllUnsupported(1); + RequireLockAllUnsupported(2); + RequireLockAllUnsupported(1 | 2); return 0; } diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index 417d795453..6c70763d2a 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -18,6 +18,8 @@ int APS5_VABI sceKernelGetModuleList(KernelModule*, std::size_t, std::size_t*); int APS5_VABI sceKernelGetModuleInfo(KernelModule, ModuleInfo*); int APS5_VABI sceKernelGetModuleList2(KernelModule*, std::size_t, std::size_t*); int APS5_VABI sceKernelGetModuleInfo2(KernelModule, ModuleInfo*); +std::int32_t APS5_VABI sceKernelGetLibkernelTextLocation_nid_postfix(void**, std::size_t*); +std::int32_t APS5_VABI sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(InternalSegmentInfo*); } static void Require(bool value) { if (!value) std::abort(); } template @@ -96,6 +98,20 @@ int main(int argc, char** argv) { Require(sceKernelGetModuleInfo(handle, &listed) == 0 && listed.name[0] != '\0'); if (handle == self.id) Require(std::string(listed.name) == self.name); } + ModuleInfo program{}; + program.st_size = sizeof(ModuleInfo); + Require(sceKernelGetModuleInfo(self.id, &program) == 0 && program.segment_count > 0); + void* textAddress = nullptr; + std::size_t textSize = 0; + Require(sceKernelGetLibkernelTextLocation_nid_postfix(&textAddress, &textSize) == 0); + Require(reinterpret_cast(textAddress) == program.segments[0].address); + Require(textSize == program.segments[0].size); + Require(ThrowsInvalidArgument([&] { sceKernelGetLibkernelTextLocation_nid_postfix(nullptr, &textSize); })); + Require(ThrowsInvalidArgument([&] { sceKernelGetLibkernelTextLocation_nid_postfix(&textAddress, nullptr); })); + InternalSegmentInfo segment{}; + Require(sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(&segment) == 0); + Require(segment.address == program.segments[0].address && segment.size == program.segments[0].size); + Require(ThrowsInvalidArgument([] { sceKernelInternalMemoryGetModuleSegmentInfo_nid_postfix(nullptr); })); for (const std::uint64_t size : {0x158, 0x1a8, 0x1b0, 0x200, 0}) { ModuleInfo sized{}; sized.st_size = size; diff --git a/core/libs/tests/GuestNetCtlApDialog.cpp b/core/libs/tests/GuestNetCtlApDialog.cpp new file mode 100644 index 0000000000..bdce6ca102 --- /dev/null +++ b/core/libs/tests/GuestNetCtlApDialog.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlApDialogInitialize(void); +int APS5_VABI sceNetCtlApDialogTerminate(void); +int APS5_VABI sceNetCtlApDialogOpen(const void* param); +int APS5_VABI sceNetCtlApDialogClose(void); +int APS5_VABI sceNetCtlApDialogUpdateStatus(void); +int APS5_VABI sceNetCtlApDialogGetStatus(void); +int APS5_VABI sceNetCtlApDialogGetResult(void* result); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +void RequireCanceled(const std::uint8_t (&result)[44]) { + std::int32_t code = 0; + std::memcpy(&code, result, sizeof(code)); + Require(code == kResultUserCanceled); + for (int i = 4; i < 40; ++i) Require(result[i] == 0); + for (int i = 40; i < 44; ++i) Require(result[i] == 0x5a); +} + +} + +int main() { + std::uint8_t param[104] = {}; + std::uint8_t result[44]; + std::memset(result, 0x5a, sizeof(result)); + + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogOpen(param) == kErrNotInitialized); + Require(sceNetCtlApDialogClose() == kErrNotInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + Require(sceNetCtlApDialogUpdateStatus() == kStatusNone); + + Require(sceNetCtlApDialogInitialize() == 0); + Require(sceNetCtlApDialogInitialize() == kErrAlreadyInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogUpdateStatus() == kStatusInitialized); + + Require(sceNetCtlApDialogOpen(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetStatus() == kStatusInitialized); + Require(sceNetCtlApDialogGetResult(result) == kErrNotFinished); + + Require(sceNetCtlApDialogOpen(param) == 0); + Require(sceNetCtlApDialogGetStatus() == kStatusFinished); + Require(sceNetCtlApDialogUpdateStatus() == kStatusFinished); + + Require(sceNetCtlApDialogGetResult(nullptr) == kErrArgNull); + Require(sceNetCtlApDialogGetResult(result) == 0); + RequireCanceled(result); + + Require(sceNetCtlApDialogOpen(param) == 0); + Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogTerminate() == 0); + Require(sceNetCtlApDialogTerminate() == kErrNotInitialized); + Require(sceNetCtlApDialogGetStatus() == kStatusNone); + + Require(sceNetCtlApDialogInitialize() == 0); + Require(sceNetCtlApDialogClose() == 0); + Require(sceNetCtlApDialogGetStatus() == kStatusFinished); + std::memset(result, 0x5a, sizeof(result)); + Require(sceNetCtlApDialogGetResult(result) == 0); + RequireCanceled(result); + Require(sceNetCtlApDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestNpAuth.cpp b/core/libs/tests/GuestNpAuth.cpp new file mode 100644 index 0000000000..f99ceafb24 --- /dev/null +++ b/core/libs/tests/GuestNpAuth.cpp @@ -0,0 +1,110 @@ +#include "SceTypes.hpp" + +#include +#include + +extern "C" { +int APS5_VABI sceNpAuthAbortRequest(int req_id); +int APS5_VABI sceNpAuthCreateAsyncRequest(const void* param); +int APS5_VABI sceNpAuthCreateRequest(void); +int APS5_VABI sceNpAuthDeleteRequest(int req_id); +int APS5_VABI sceNpAuthGetAuthorizationCodeV3(int req_id, const void* param, void* auth_code, int* issuer_id); +int APS5_VABI sceNpAuthGetIdTokenV3(int req_id, const void* param, void* id_token); +int APS5_VABI sceNpAuthPollAsync(int req_id, int* result); +int APS5_VABI sceNpAuthSetTimeout(int req_id, int resolve_retry, uint32_t resolve_timeout, uint32_t connect_timeout, uint32_t send_timeout, uint32_t receive_timeout); +int APS5_VABI sceNpAuthWaitAsync(int req_id, int* result); +} + +static void Require(bool condition) { + if (!condition) { + std::abort(); + } +} + +int main() { + constexpr int invalidArgument = static_cast(0x80550301); + constexpr int signedOut = static_cast(0x80550006); + constexpr int aborted = static_cast(0x80550304); + constexpr int requestNotFound = static_cast(0x80550306); + constexpr int invalidId = static_cast(0x80550307); + + const int syncReq = sceNpAuthCreateRequest(); + const int syncReq2 = sceNpAuthCreateRequest(); + Require(syncReq > 0); + Require(syncReq2 > syncReq); + + const int asyncReq = sceNpAuthCreateAsyncRequest(nullptr); + const int asyncReq2 = sceNpAuthCreateAsyncRequest(nullptr); + Require(asyncReq > syncReq2); + Require(asyncReq2 > asyncReq); + + alignas(8) char param[16] = {}; + alignas(8) char token[16] = {}; + alignas(8) char authCode[16] = {}; + int issuerId = 0; + + Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == 0); + Require(sceNpAuthSetTimeout(asyncReq, 0, 0, 0, 0, 0) == 0); + + int pollUnprocessed = 0; + Require(sceNpAuthPollAsync(asyncReq, &pollUnprocessed) == invalidId); + int waitUnprocessed = 0; + Require(sceNpAuthWaitAsync(asyncReq, &waitUnprocessed) == invalidId); + + Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, token) == invalidArgument); + Require(sceNpAuthGetIdTokenV3(syncReq, param, nullptr) == invalidArgument); + Require(sceNpAuthGetIdTokenV3(syncReq, nullptr, nullptr) == invalidArgument); + Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == signedOut); + Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeV3(syncReq, nullptr, authCode, &issuerId) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeV3(syncReq, param, nullptr, &issuerId) == invalidArgument); + Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == signedOut); + Require(sceNpAuthGetAuthorizationCodeV3(syncReq2, param, authCode, &issuerId) == invalidArgument); + + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == 0); + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); + int asyncPoll = 0; + Require(sceNpAuthPollAsync(asyncReq, &asyncPoll) == 0); + Require(asyncPoll == signedOut); + int asyncWait = 0; + Require(sceNpAuthWaitAsync(asyncReq, &asyncWait) == 0); + Require(asyncWait == signedOut); + + Require(sceNpAuthGetAuthorizationCodeV3(asyncReq2, param, authCode, &issuerId) == 0); + Require(sceNpAuthGetAuthorizationCodeV3(asyncReq2, param, authCode, &issuerId) == invalidArgument); + int asyncWait2 = 0; + Require(sceNpAuthWaitAsync(asyncReq2, &asyncWait2) == 0); + Require(asyncWait2 == signedOut); + + constexpr int unknownReq = 999999; + Require(sceNpAuthGetIdTokenV3(unknownReq, param, token) == requestNotFound); + Require(sceNpAuthGetAuthorizationCodeV3(unknownReq, param, authCode, &issuerId) == requestNotFound); + int pollResult = 0; + Require(sceNpAuthPollAsync(unknownReq, &pollResult) == requestNotFound); + int waitResult = 0; + Require(sceNpAuthWaitAsync(unknownReq, &waitResult) == requestNotFound); + Require(sceNpAuthSetTimeout(unknownReq, 2, 5000000, 10000000, 10000000, 10000000) == requestNotFound); + + Require(sceNpAuthWaitAsync(syncReq, nullptr) == invalidArgument); + Require(sceNpAuthWaitAsync(syncReq, &waitResult) == invalidId); + Require(sceNpAuthPollAsync(syncReq, &pollResult) == invalidId); + + Require(sceNpAuthAbortRequest(asyncReq) == 0); + Require(sceNpAuthGetIdTokenV3(asyncReq, param, token) == invalidArgument); + Require(sceNpAuthAbortRequest(unknownReq) == requestNotFound); + + const int abortedReq = sceNpAuthCreateAsyncRequest(nullptr); + Require(sceNpAuthAbortRequest(abortedReq) == 0); + Require(sceNpAuthGetIdTokenV3(abortedReq, param, token) == aborted); + Require(sceNpAuthGetAuthorizationCodeV3(abortedReq, param, authCode, &issuerId) == aborted); + int abortedWait = 0; + Require(sceNpAuthWaitAsync(abortedReq, &abortedWait) == 0); + Require(abortedWait == aborted); + + Require(sceNpAuthDeleteRequest(syncReq) == 0); + Require(sceNpAuthGetIdTokenV3(syncReq, param, token) == requestNotFound); + Require(sceNpAuthWaitAsync(syncReq, &waitResult) == requestNotFound); + Require(sceNpAuthSetTimeout(syncReq, 2, 5000000, 10000000, 10000000, 10000000) == requestNotFound); + + return 0; +} diff --git a/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp new file mode 100644 index 0000000000..f6dfce0de8 --- /dev/null +++ b/core/libs/tests/GuestNpAuthAuthorizedAppCode.cpp @@ -0,0 +1,13 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +int APS5_VABI sceNpAuthGetAuthorizedAppCode(); +} + +int main() { + constexpr int kSignedOut = static_cast(0x80550006); + if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); + if (sceNpAuthGetAuthorizedAppCode() != kSignedOut) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestNpPartner.cpp b/core/libs/tests/GuestNpPartner.cpp new file mode 100644 index 0000000000..0121f30c88 --- /dev/null +++ b/core/libs/tests/GuestNpPartner.cpp @@ -0,0 +1,34 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceNpEAAccessInitialize(void); +int APS5_VABI sceNpEAAccessTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TAction action) { + try { + action(); + } catch (const std::logic_error&) { + return true; + } + return false; +} + +} + +int main() { + Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(sceNpEAAccessInitialize() == 0); + Require(Throws([] { sceNpEAAccessInitialize(); })); + Require(sceNpEAAccessTerminate() == 0); + Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(sceNpEAAccessInitialize() == 0); + Require(sceNpEAAccessTerminate() == 0); +} diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 834f15fb46..27bc077144 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceNpSessionSignalingInitialize(void* param); int APS5_VABI sceNpSessionSignalingCreateContext2(const void* param, std::uint32_t* contextId); std::int32_t APS5_VABI sceNpSessionSignalingGetLocalNetInfo(std::int32_t contextId, void* info); int APS5_VABI sceNpSessionSignalingRequestPrepare(std::uint32_t contextId, std::uint32_t* requestId); +std::int32_t APS5_VABI sceNpSessionSignalingGetConnectionStatus(std::int32_t contextId, std::int32_t connectionId, std::int32_t* status, void* peerAddress, std::uint16_t* peerPort); int APS5_VABI sceNpSessionSignalingTerminate(void); } @@ -16,6 +17,7 @@ namespace { constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); +constexpr int ConnectionNotFound = static_cast(0x8055330Cu); void Require(bool condition, const char* message) { if (!condition) { @@ -47,5 +49,13 @@ int main() { Require(sceNpSessionSignalingGetLocalNetInfo(netContextId, info) == Unavailable, "local net info without network"); Require(std::memcmp(info, untouched, sizeof(info)) == 0, "failed query modified the info"); + std::int32_t status = 0x5a5a5a5a; + std::uint32_t peerAddress = 0xa5a5a5a5u; + std::uint16_t peerPort = 0xa5a5; + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, nullptr, &peerAddress, &peerPort) == InvalidArgument, "null connection status"); + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, &peerAddress, &peerPort) == ConnectionNotFound, "connection without network"); + Require(sceNpSessionSignalingGetConnectionStatus(netContextId, 1, &status, nullptr, nullptr) == ConnectionNotFound, "connection without peer outputs"); + Require(status == 0x5a5a5a5a && peerAddress == 0xa5a5a5a5u && peerPort == 0xa5a5, "failed status query wrote an output"); + Require(sceNpSessionSignalingTerminate() == 0, "terminate failed"); } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index ea167e6548..3f31d7d99f 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -15,6 +15,8 @@ extern "C" { int APS5_VABI sceNpWebApi2Initialize(int, std::size_t); 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); } static void Require(bool value) { if (!value) std::abort(); } @@ -38,8 +40,12 @@ int main() { Require(stats.pool_size == 0x20000 && stats.max_inuse_size == 0 && stats.current_inuse_size == 0 && stats.reserved == 0); Require(sceNpWebApi2GetMemoryPoolStats(other, &stats) == 0 && stats.pool_size == 0x8000); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, nullptr); })); + const int handle = sceNpWebApi2PushEventCreateHandle(context); + Require(handle > 0); + Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); Require(sceNpWebApi2Terminate(context) == 0); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); + Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); Require(sceNpWebApi2Terminate(other) == 0); return 0; } diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 6f064ed626..32dcdd9acf 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -13,6 +13,8 @@ int APS5_VABI sceHttpSetCookieEnabled(int, int); int APS5_VABI sceHttpSendRequest(int, const void*, std::size_t); int APS5_VABI sceNpEntitlementAccessGetEntitlementKey( std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessEntitlementKey*); +int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetPftFlag(std::uint32_t*); int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(); @@ -24,7 +26,7 @@ int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpActivate(); int APS5_VABI sceRudpGetStatus(void*, std::size_t); -int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -66,6 +68,16 @@ int main() { Require(std::memcmp(&output, original.data(), sizeof(output)) == 0); } + std::array trialsFlag{0xa5a5a5a5u, 0x5a5a5a5au}; + Require(sceNpEntitlementAccessGetGameTrialsFlag(trialsFlag.data()) == 0); + Require(trialsFlag[0] == 0 && trialsFlag[1] == 0x5a5a5a5au); + Require(sceNpEntitlementAccessGetGameTrialsFlag(nullptr) == parameter); + + std::array pftFlag{0xa5a5a5a5u, 0x5a5a5a5au}; + Require(sceNpEntitlementAccessGetPftFlag(pftFlag.data()) == 0); + Require(pftFlag[0] == 0 && pftFlag[1] == 0x5a5a5a5au); + Require(sceNpEntitlementAccessGetPftFlag(nullptr) == parameter); + constexpr int signedOut = static_cast(0x80550006); Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); @@ -89,6 +101,6 @@ int main() { Require(sceRudpGetStatus(status.data(), status.size()) == 0); for (unsigned char byte : status) Require(byte == 0); Require(sceRudpGetStatus(nullptr, 0) == static_cast(0x80770004)); - Require(sceRudpTerminate() == 0); + Require(sceRudpEnd() == 0); return 0; } diff --git a/core/libs/tests/GuestPPSA01530Exports.cpp b/core/libs/tests/GuestPPSA01530Exports.cpp index 23f05249eb..aeafa78f9c 100644 --- a/core/libs/tests/GuestPPSA01530Exports.cpp +++ b/core/libs/tests/GuestPPSA01530Exports.cpp @@ -8,8 +8,6 @@ extern "C" { int APS5_VABI sceNetShowIfconfig(); int APS5_VABI scePadUnknown_n3kSX62fgNo(); int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_(); -int APS5_VABI kqueue_nid_postfix(); -int APS5_VABI kevent_nid_postfix(); } namespace { @@ -23,8 +21,6 @@ const Export exports[] = { {"sceNetShowIfconfig", sceNetShowIfconfig}, {"scePadUnknown_n3kSX62fgNo", scePadUnknown_n3kSX62fgNo}, {"_ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_", _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS2_PvES6_}, - {"kqueue_nid_postfix", kqueue_nid_postfix}, - {"kevent_nid_postfix", kevent_nid_postfix}, }; bool ReportsNotImplemented(const Export& entry) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 4eb1d8b0ba..9fa2e91b41 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -113,6 +113,24 @@ static void CheckTiltCorrection(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckAngularVelocityDeadband(int handle) { + PadInputState moving; + moving.hasMotion = true; + moving.accel = {0.0f, 9.80665f, 0.0f}; + moving.gyro = {0.01f, 0.0175f, -0.017f}; + PadPublishInput_nid_postfix(moving); + PadData data{}; + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.01f && data.angular_velocity_y == 0.0175f && data.angular_velocity_z == -0.017f); + Require(scePadSetAngularVelocityDeadbandState(handle, true) == PAD_OK); + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.0f && data.angular_velocity_y == 0.0175f && data.angular_velocity_z == 0.0f); + Require(scePadSetAngularVelocityDeadbandState(handle, false) == PAD_OK); + Require(scePadReadState(handle, &data) == PAD_OK); + Require(data.angular_velocity_x == 0.01f && data.angular_velocity_z == -0.017f); + PadPublishInput_nid_postfix(PadInputState{}); +} + static void CheckTouchContact() { PadInputState touch; touch.buttons = static_cast(Pad::PadButton::TouchPad); @@ -253,6 +271,7 @@ int main() { Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); Require(scePadGetHandle(user, 0, 0) == handle); CheckTiltCorrection(handle); + CheckAngularVelocityDeadband(handle); CheckTouchContact(); CheckPartialTrigger(); CheckReadQueue(handle); @@ -272,6 +291,7 @@ int main() { Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/core/libs/tests/GuestParentProcess.cpp b/core/libs/tests/GuestParentProcess.cpp index c705785c23..203cfe3a00 100644 --- a/core/libs/tests/GuestParentProcess.cpp +++ b/core/libs/tests/GuestParentProcess.cpp @@ -8,9 +8,19 @@ extern "C" { int APS5_VABI getpid_nid_postfix(void); int APS5_VABI getppid_nid_postfix(void); +int APS5_VABI getsid_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } int main() { + const int self = getpid_nid_postfix(); + Require(getsid_nid_postfix(0) == self); + Require(getsid_nid_postfix(self) == self); + *__error_nid_postfix() = 0; + Require(getsid_nid_postfix(123456) == -1); + Require(*__error_nid_postfix() == 3); + Require(getsid_nid_postfix(-1) == -1); + Require(*__error_nid_postfix() == 3); const int parent = getppid_nid_postfix(); Require(parent > 0 && parent != getpid_nid_postfix()); Require(getppid_nid_postfix() == parent); 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/GuestPngDec.cpp b/core/libs/tests/GuestPngDec.cpp index 6bb53f9f6b..f9536a50a2 100644 --- a/core/libs/tests/GuestPngDec.cpp +++ b/core/libs/tests/GuestPngDec.cpp @@ -76,6 +76,8 @@ int main() { const std::int32_t memorySize = scePngDecQueryMemorySize(¶m); Require(memorySize > 0); Require(scePngDecQueryMemorySize(nullptr) == invalidParam); + const PngDecCreateParam badSize{sizeof(PngDecCreateParam) - 1, 0, 1920}; + Require(scePngDecQueryMemorySize(&badSize) == invalidSize); const PngDecCreateParam badAttribute{sizeof(PngDecCreateParam), 2, 1920}; Require(scePngDecQueryMemorySize(&badAttribute) == invalidParam); const PngDecCreateParam zeroWidth{sizeof(PngDecCreateParam), 0, 0}; @@ -89,6 +91,7 @@ int main() { unsigned char* unaligned = memory.data() + 1; void* handle = nullptr; Require(scePngDecCreate(nullptr, unaligned, static_cast(memorySize), &handle) == invalidParam); + Require(scePngDecCreate(&badSize, unaligned, static_cast(memorySize), &handle) == invalidSize); Require(scePngDecCreate(¶m, nullptr, static_cast(memorySize), &handle) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize), nullptr) == invalidAddr); Require(scePngDecCreate(¶m, unaligned, static_cast(memorySize) - 1, &handle) == invalidWorkMemory); diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index a32abd9a69..37171623de 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -1,8 +1,20 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include #include +#include +#include +#include struct PollDescriptor { int descriptor; short events; @@ -18,15 +30,107 @@ int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); +int APS5_VABI pipe_nid_postfix(int*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } +#ifdef _WIN32 +using NativeSocket = SOCKET; +constexpr NativeSocket NativeInvalid = INVALID_SOCKET; +static void CloseNative(NativeSocket socket) { closesocket(socket); } +#else +using NativeSocket = int; +constexpr NativeSocket NativeInvalid = -1; +static void CloseNative(NativeSocket socket) { close(socket); } +#endif static void Require(bool value) { if (!value) std::abort(); } constexpr short In = 0x1; +constexpr short Priority = 0x2; +constexpr short ReadBand = 0x80; constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; -int main() { +constexpr short ReadNormal = 0x40; +static void TestPipes(int socket) { + int ends[2]; + Require(pipe_nid_postfix(ends) == 0); + PollDescriptor empty[2]{{ends[0], In, -1}, {ends[1], Out, -1}}; + Require(poll_nid_postfix(empty, 2, 0) == 1 && empty[0].revents == 0 && empty[1].revents == Out); + PollDescriptor reader{ends[0], In | ReadNormal, -1}; + const auto start = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&reader, 1, 50) == 0 && reader.revents == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + Require(write_nid_postfix(ends[1], "wake", 4) == 4); + Require(poll_nid_postfix(&reader, 1, 1000) == 1 && reader.revents == (In | ReadNormal)); + PollDescriptor writer{ends[1], In, -1}; + Require(poll_nid_postfix(&writer, 1, 0) == 0 && writer.revents == 0); + PollDescriptor readerOut{ends[0], Out, -1}; + bool threw = false; + try { poll_nid_postfix(&readerOut, 1, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + PollDescriptor mixed[2]{{socket, In, -1}, {ends[0], In, -1}}; + Require(poll_nid_postfix(mixed, 2, 1000) == 1 && mixed[0].revents == 0 && mixed[1].revents == In); + char buffer[8]{}; + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 4); + Require(poll_nid_postfix(mixed, 2, 0) == 0 && mixed[1].revents == 0); + std::thread late([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(write_nid_postfix(ends[1], "x", 1) == 1); + }); + Require(poll_nid_postfix(mixed, 2, 2000) == 1 && mixed[1].revents == In); + late.join(); + std::thread lateInfinite([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + Require(write_nid_postfix(ends[1], "y", 1) == 1); + }); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + const auto waitStart = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&reader, 1, -1) == 1 && reader.revents == (In | ReadNormal)); + Require(std::chrono::steady_clock::now() - waitStart >= std::chrono::milliseconds(20)); + lateInfinite.join(); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + Require(write_nid_postfix(ends[1], "z", 1) == 1); + Require(close_nid_postfix(ends[1]) == 0); + Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup)); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1); + Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup)); + Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 0); + PollDescriptor closedPipe{ends[1], In, -1}; + Require(poll_nid_postfix(&closedPipe, 1, 0) == 1 && closedPipe.revents == Invalid); + Require(close_nid_postfix(ends[0]) == 0); + int full[2]; + Require(pipe_nid_postfix(full) == 0); + static char block[4096]; + std::int64_t queued = 0; + PollDescriptor fill{full[1], Out, -1}; + while (poll_nid_postfix(&fill, 1, 0) == 1) { + Require(fill.revents == Out && queued < (1 << 20)); + Require(write_nid_postfix(full[1], block, 256) == 256); + queued += 256; + } + Require(queued > 0 && fill.revents == 0); + while (queued > 0) { + const std::int64_t taken = read_nid_postfix(full[0], block, static_cast(std::min(queued, sizeof(block)))); + Require(taken > 0); + queued -= taken; + } + Require(poll_nid_postfix(&fill, 1, 0) == 1 && fill.revents == Out); + Require(close_nid_postfix(full[0]) == 0); + Require(close_nid_postfix(full[1]) == 0); + int other[2]; + Require(pipe_nid_postfix(other) == 0); + Require(close_nid_postfix(other[0]) == 0); + PollDescriptor orphan{other[1], Out, -1}; + Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == Hup); + orphan.events = In | Out; + Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == (In | Hup)); + Require(close_nid_postfix(other[1]) == 0); +} +static void PollsUrgentData() { const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; @@ -34,6 +138,53 @@ int main() { std::uint32_t size = address.size(); Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); Require(listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0); + if (connect_nid_postfix(client, address.data(), address.size()) != 0) + Require(*__error_nid_postfix() == 35 || *__error_nid_postfix() == 36); + PollDescriptor connecting{client, Priority | Out, -1}; + Require(poll_nid_postfix(&connecting, 1, 1000) == 1 && connecting.revents == Out); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + PollDescriptor pair[2]{{server, Priority | Out, -1}, {client, In | Priority | Out | ReadBand, -1}}; + Require(poll_nid_postfix(pair, 2, 0) == 2 && pair[0].revents == Out && pair[1].revents == Out); + char byte = 'u'; + Require(send_nid_postfix(client, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + Require(recv_nid_postfix(server, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + const NativeSocket peer = ::socket(AF_INET, SOCK_STREAM, 0); + Require(peer != NativeInvalid); + sockaddr_in native{}; + native.sin_family = AF_INET; + std::memcpy(&native.sin_port, address.data() + 2, 2); + std::memcpy(&native.sin_addr, address.data() + 4, 4); + Require(::connect(peer, reinterpret_cast(&native), sizeof(native)) == 0); + const int urgent = accept_nid_postfix(listener, nullptr, nullptr); + Require(urgent >= 0); + PollDescriptor waiting{urgent, Priority, -1}; + Require(poll_nid_postfix(&waiting, 1, 0) == 0 && waiting.revents == 0); + Require(::send(peer, "u", 1, MSG_OOB) == 1); + Require(poll_nid_postfix(&waiting, 1, 1000) == 1 && waiting.revents == Priority); + PollDescriptor bands{urgent, In | Priority | Out, -1}; + Require(poll_nid_postfix(&bands, 1, 0) == 1 && bands.revents == (Priority | Out)); +#ifdef _WIN32 + PollDescriptor band{urgent, ReadBand, -1}; + Require(poll_nid_postfix(&band, 1, 0) == 1 && band.revents == ReadBand); +#endif + CloseNative(peer); + Require(close_nid_postfix(urgent) == 0); + Require(close_nid_postfix(server) == 0); + Require(close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0); +} +int main() { + PollsUrgentData(); + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0 && listener < 1024); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + Require(listen_nid_postfix(listener, 4) == 0); PollDescriptor single{listener, In, -1}; Require(poll_nid_postfix(&single, 1, 0) == 0 && single.revents == 0); const auto start = std::chrono::steady_clock::now(); @@ -61,6 +212,18 @@ int main() { Require(poll_nid_postfix(nullptr, 0, 10) == 0); 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); Require(close_nid_postfix(server) == 0); + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + PollDescriptor mixed[2]{{listener, In, 0}, {file, In, 0}}; + bool unsupported = false; + try { + poll_nid_postfix(mixed, 2, 0); + } catch (const std::runtime_error&) { + unsupported = true; + } + Require(unsupported); + Require(close_nid_postfix(file) == 0); Require(close_nid_postfix(listener) == 0); } 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/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp index 540ca69d5d..0e3326d3d0 100644 --- a/core/libs/tests/GuestPosixFilesystem.cpp +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -1,5 +1,4 @@ #include "prx/libc/include/General.hpp" -#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include #include @@ -111,7 +110,7 @@ int main() { Require(close_nid_postfix(pipe) == 0); } const int socket = socket_nid_postfix(2, 2, 0); - Require(socket >= GuestSockets::FirstDescriptor); + Require(socket >= 0 && socket < 1024); Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 25); Require(close_nid_postfix(socket) == 0); Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 9); diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp new file mode 100644 index 0000000000..8fe49f711d --- /dev/null +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceLibcInternal/include/Spawn.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); +int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); +int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions**); +int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions**); +int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions**, int, const char*, int, std::uint16_t); +int APS5_VABI posix_spawn_file_actions_adddup2_nid_postfix(PosixSpawnFileActions**, int, int); +int APS5_VABI posix_spawn_file_actions_addclose_nid_postfix(PosixSpawnFileActions**, int); +int APS5_VABI posix_spawn_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +int APS5_VABI posix_spawnp_nid_postfix(std::int32_t*, const char*, PosixSpawnFileActions* const*, PosixSpawnAttributes* const*, char* const*, char* const*); +} + +static void Require(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "Posix spawn check failed at line %d\n", line); + std::abort(); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int BadDescriptor = 9; + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template +static bool ReportsNotImplemented(TCall call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == message; + } + return false; +} + +static void CheckAttributes() { + PosixSpawnAttributes* attributes = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && attributes != nullptr); + Check(attributes->flags == 0 && attributes->processGroup == 0 && attributes->schedulingPriority == 0 && attributes->schedulingPolicy == 0); + const GuestSignalSet empty{}; + Check(std::memcmp(&attributes->signalDefault, &empty, sizeof(empty)) == 0 && std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0x30) == 0 && attributes->flags == 0x30); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, -1) == 0 && attributes->flags == -1); + Check(posix_spawnattr_setflags_nid_postfix(&attributes, 0) == 0 && attributes->flags == 0); + + const GuestSignalSet defaults{{0x00004002u, 0x80000000u, 1u, 0xffffffffu}}; + Check(posix_spawnattr_setsigdefault_nid_postfix(&attributes, &defaults) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + + PosixSpawnAttributes* uninitialized = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setflags_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&uninitialized, &defaults); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([] { posix_spawnattr_init_nid_postfix(nullptr); })); + + Check(posix_spawnattr_destroy_nid_postfix(&attributes) == 0); + Check(posix_spawnattr_destroy_nid_postfix(&uninitialized) == 0); +} + +static void CheckFileActions() { + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawn_file_actions_init_nid_postfix(&actions) == 0 && actions != nullptr && actions->actions.empty()); + + char path[] = "/app0/log.txt"; + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, 3, path, 0x601, 0644) == 0); + std::strcpy(path, "/changed"); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 3, 1) == 0); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, 3) == 0); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 0, 0) == 0); + + Check(posix_spawn_file_actions_addopen_nid_postfix(&actions, -1, "/app0/other", 0, 0) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, -1, 1) == BadDescriptor); + Check(posix_spawn_file_actions_adddup2_nid_postfix(&actions, 1, -1) == BadDescriptor); + Check(posix_spawn_file_actions_addclose_nid_postfix(&actions, -2) == BadDescriptor); + + const auto& recorded = actions->actions; + Check(recorded.size() == 4); + Check(recorded[0].kind == PosixSpawnFileActionKind::Open && recorded[0].descriptor == 3); + Check(recorded[0].path == "/app0/log.txt" && recorded[0].openFlags == 0x601 && recorded[0].mode == 0644); + Check(recorded[1].kind == PosixSpawnFileActionKind::Duplicate && recorded[1].descriptor == 3 && recorded[1].newDescriptor == 1); + Check(recorded[2].kind == PosixSpawnFileActionKind::Close && recorded[2].descriptor == 3); + Check(recorded[3].kind == PosixSpawnFileActionKind::Duplicate && recorded[3].descriptor == 0 && recorded[3].newDescriptor == 0); + + PosixSpawnFileActions* uninitialized = nullptr; + Check(posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, -1) == BadDescriptor); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addclose_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_adddup2_nid_postfix(&uninitialized, 1, 2); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&uninitialized, 1, "/app0/a", 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_addopen_nid_postfix(&actions, 1, nullptr, 0, 0); })); + Check(ThrowsInvalidArgument([&] { posix_spawn_file_actions_destroy_nid_postfix(&uninitialized); })); + Check(ThrowsInvalidArgument([] { posix_spawn_file_actions_init_nid_postfix(nullptr); })); + Check(actions->actions.size() == 4); + + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0); +} + +static void CheckSpawn() { + PosixSpawnAttributes* attributes = nullptr; + PosixSpawnFileActions* actions = nullptr; + Check(posix_spawnattr_init_nid_postfix(&attributes) == 0 && posix_spawn_file_actions_init_nid_postfix(&actions) == 0); + char program[] = "/app0/tool.elf"; + char* arguments[] = {program, nullptr}; + char* environment[] = {nullptr}; + std::int32_t processId = 77; + Check(ReportsNotImplemented([&] { posix_spawn_nid_postfix(&processId, program, &actions, &attributes, arguments, environment); }, + "posix_spawn: process creation not implemented")); + Check(ReportsNotImplemented([&] { posix_spawnp_nid_postfix(&processId, "tool.elf", nullptr, nullptr, arguments, nullptr); }, + "posix_spawnp: process creation not implemented")); + Check(processId == 77); + Check(posix_spawn_file_actions_destroy_nid_postfix(&actions) == 0 && posix_spawnattr_destroy_nid_postfix(&attributes) == 0); +} + +int main() { + CheckAttributes(); + CheckFileActions(); + CheckSpawn(); +} diff --git a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp new file mode 100644 index 0000000000..e5a76a3bb8 --- /dev/null +++ b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp @@ -0,0 +1,53 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceProprietaryVoiceChatHelperInitialize(); +int APS5_VABI sceProprietaryVoiceChatHelperTerminate(); +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); +} + +static void Require(bool value, const char* what) { + if (value) return; + std::printf("ProprietaryVoiceChatHelper: %s\n", what); + std::abort(); +} + +template +static bool Throws(TCall call) { + try { + call(); + } catch (const TError&) { + return true; + } + return false; +} + +int main() { + Require(Throws([] { sceProprietaryVoiceChatHelperSetVoiceChatState(); }), + "setting the state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), + "reading the usage state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperTerminate(); }), + "terminating before initialization did not throw"); + + Require(sceProprietaryVoiceChatHelperInitialize() == 0, "initialization failed"); + Require(Throws([] { sceProprietaryVoiceChatHelperInitialize(); }), + "initializing twice did not throw"); + Require(sceProprietaryVoiceChatHelperSetVoiceChatState() == 0, "setting the state failed"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), + "reading the unknown usage state did not throw"); + Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination failed"); + + Require(Throws([] { sceProprietaryVoiceChatHelperSetVoiceChatState(); }), + "setting the state after termination did not throw"); + Require(sceProprietaryVoiceChatHelperInitialize() == 0, "initialization after termination failed"); + Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination after reinitialization failed"); + + std::puts("ProprietaryVoiceChatHelper tests passed"); + return 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/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 503e2dc69f..4680fca3bf 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -233,7 +233,84 @@ static void CheckDeadlinesBeyondTheClockRange() { Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); } +struct WaitingWriter { + PthreadRwlock* rwlock; + std::atomic started{false}; + std::atomic acquired{false}; + Pthread thread = nullptr; +}; + +static void* APS5_VABI WriteOnceStarted(void* arg) { + auto& writer = *static_cast(arg); + writer.started.store(true); + Require(pthread_rwlock_wrlock_nid_postfix(writer.rwlock) == 0); + writer.acquired.store(true); + Require(pthread_rwlock_unlock_nid_postfix(writer.rwlock) == 0); + return nullptr; +} + +static void StartWaitingWriter(WaitingWriter& writer) { + Require(scePthreadCreate(&writer.thread, nullptr, WriteOnceStarted, &writer, nullptr) == SCE_OK); + while (!writer.started.load()) std::this_thread::yield(); +} + +static int TryReadUntilRefused(PthreadRwlock* rwlock) { + for (int attempt = 0; attempt < 2000; ++attempt) { + const int result = pthread_rwlock_tryrdlock_nid_postfix(rwlock); + if (result != 0) return result; + Require(pthread_rwlock_unlock_nid_postfix(rwlock) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return 0; +} + +struct RefusalProbe { + PthreadRwlock* rwlock; + std::atomic result{-1}; +}; + +static void* APS5_VABI ProbeRefusal(void* arg) { + auto& probe = *static_cast(arg); + probe.result.store(TryReadUntilRefused(probe.rwlock)); + return nullptr; +} + +static void CheckWaitingWriterHoldsBackNewReaders() { + PthreadRwlock rwlock = nullptr; + Holder reader{&rwlock, false}; + Start(reader); + WaitingWriter writer{&rwlock}; + StartWaitingWriter(writer); + Require(TryReadUntilRefused(&rwlock) == GUEST_EBUSY); + const KernelTimespec soon = After(20); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &soon) == GUEST_ETIMEDOUT); + Require(!writer.acquired.load()); + reader.release.store(true); + Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); + Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); + Require(writer.acquired.load()); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); + + PthreadRwlock recursive = nullptr; + Require(pthread_rwlock_rdlock_nid_postfix(&recursive) == 0); + WaitingWriter queued{&recursive}; + StartWaitingWriter(queued); + RefusalProbe probe{&recursive}; + Pthread prober = nullptr; + Require(scePthreadCreate(&prober, nullptr, ProbeRefusal, &probe, nullptr) == SCE_OK); + Require(scePthreadJoin(prober, nullptr) == SCE_OK); + Require(probe.result.load() == GUEST_EBUSY); + Require(pthread_rwlock_rdlock_nid_postfix(&recursive) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&recursive) == 0); + Require(!queued.acquired.load()); + Require(pthread_rwlock_unlock_nid_postfix(&recursive) == 0); + Require(scePthreadJoin(queued.thread, nullptr) == SCE_OK); + Require(queued.acquired.load()); + Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); +} + int main() { + CheckWaitingWriterHoldsBackNewReaders(); CheckConcurrentFirstWrlockExcludes(); CheckDeadlinesBeyondTheClockRange(); const KernelTimespec invalid{0, NANOS_PER_SECOND}; diff --git a/core/libs/tests/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp new file mode 100644 index 0000000000..6b6b07b2b8 --- /dev/null +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param); +int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param); +int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param); +int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int GUEST_EINVAL = 22; +static constexpr int SCHED_FIFO = 1; +static constexpr int SCHED_OTHER = 2; +static constexpr int SCHED_RR = 3; +static constexpr int EXPLICIT_SCHED = 0; +static constexpr int INHERIT_SCHED = 4; + +static void Require(bool value) { if (!value) std::abort(); } + +static void RequireSched(int policy, int priority) { + int current = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), ¤t, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + current = -1; + param.sched_priority = -1; + Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); + Require(current == policy && param.sched_priority == priority); +} + +static int SceSet(int policy, int priority) { + const KernelSchedParam param{priority}; + return scePthreadSetschedparam(scePthreadSelf(), policy, ¶m); +} + +static int PosixSet(int policy, int priority) { + const KernelSchedParam param{priority}; + return pthread_setschedparam_nid_postfix(scePthreadSelf(), policy, ¶m); +} + +static void* APS5_VABI Run(void*) { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + Require(policy == SCHED_FIFO); + + for (int priority : {256, 767, 896, 959}) { + Require(SceSet(SCHED_FIFO, priority) == SCE_OK); + RequireSched(SCHED_FIFO, priority); + } + for (int priority : {-1, 0, 255, 768, 895, 960, 1023, 4096}) { + Require(SceSet(SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(SCHED_FIFO, 959); + } + + Require(SceSet(SCHED_RR, 300) == SCE_OK); + RequireSched(SCHED_RR, 300); + for (int other : {-1, 0, SCHED_OTHER, 4}) { + Require(SceSet(other, 700) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(other, 700) == GUEST_EINVAL); + RequireSched(SCHED_RR, 300); + } + Require(PosixSet(SCHED_FIFO, 700) == 0); + RequireSched(SCHED_FIFO, 700); + Require(scePthreadSetschedparam(scePthreadSelf(), SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); + return nullptr; +} + +static void* APS5_VABI ReportPolicy(void* arg) { + int policy = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); + *static_cast(arg) = policy; + return nullptr; +} + +static int CreatedPolicy(int inheritSched, int policy) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + int reported = -1; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, &attr, ReportPolicy, &reported, "policy") == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + return reported; +} + +int main() { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); + Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); + Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); +} diff --git a/core/libs/tests/GuestPthreadSem.cpp b/core/libs/tests/GuestPthreadSem.cpp index 001fe44fe5..574228ddf4 100644 --- a/core/libs/tests/GuestPthreadSem.cpp +++ b/core/libs/tests/GuestPthreadSem.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -20,10 +21,39 @@ int APS5_VABI scePthreadSemGetvalue(PthreadSem* sem, int* value); static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOVERFLOW = static_cast(0x80020054u); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static void Require(bool value) { if (!value) std::abort(); } +static void CheckValueLimit() { + constexpr unsigned int maximum = 0x7fffffffu; + PthreadSem sem = nullptr; + Require(scePthreadSemInit(&sem, 0, 7, nullptr) == SCE_OK); + const PthreadSem original = sem; + const unsigned int invalidValues[] = {maximum + 1u, std::numeric_limits::max()}; + int value = -1; + for (const unsigned int initial : invalidValues) { + Require(scePthreadSemInit(&sem, 0, initial, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sem == original); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == 7); + } + Require(scePthreadSemDestroy(&sem) == SCE_OK); + + Require(scePthreadSemInit(&sem, 0, maximum, nullptr) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemPost(&sem) == SCE_KERNEL_ERROR_EOVERFLOW); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemTrywait(&sem) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum - 1)); + Require(scePthreadSemPost(&sem) == SCE_OK); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemPost(&sem) == SCE_KERNEL_ERROR_EOVERFLOW); + Require(scePthreadSemGetvalue(&sem, &value) == SCE_OK && value == static_cast(maximum)); + Require(scePthreadSemDestroy(&sem) == SCE_OK); +} + struct Context { PthreadSem sem = nullptr; }; @@ -37,6 +67,8 @@ static void* APS5_VABI Poster(void* arg) { } int main() { + CheckValueLimit(); + PthreadSem sem = nullptr; Require(scePthreadSemInit(&sem, 0, 2, nullptr) == SCE_OK); int value = -1; diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index e4af18eede..85f04438fc 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); +int APS5_VABI sceRazorCpuSync(void); } namespace { @@ -41,4 +42,7 @@ int main() { Require(sceRazorCpuPlotValue("read time (ms)", 1.5f) == 0); Require(sceRazorCpuWriteBookmark("read timeout", "lba=0x10") == 0); Require(sceRazorCpuWriteBookmark("read timeout", nullptr) == 0); + Require(sceRazorCpuSync() == 0); + Require(sceRazorCpuSync() == 0); + Require(sceRazorCpuIsCapturing() == 0); } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index d82c887adf..96273f2b73 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -262,6 +262,9 @@ int main() { {"Thu, 01 Mar 2024 12:34:56 ", 0, march, 0}, {"Thu, 01 Mar 2024 12:34:567 EST", 0, march, 0}, {"Thu, 01 Mar 2024 12:34 EST", 0, at(2024, 3, 1, 12, 34, 0), 0}, + {"Thu, 29 Feb 2024 12:34", badParse, untouched, 0}, + {"Thu, 01 Mar 2024 12:34", badParse, untouched, 0}, + {"Thu, 29 Feb 2024 12:34 ", 0, at(2024, 2, 29, 12, 34, 0), 0}, {"Thu, 01 Mar 2024 12:34EST", badParse, untouched, 0}, {"Thu, 01 Mar 2024 1:2:3 EST", 0, at(2024, 3, 1, 1, 2, 3), -300}, {"Thu, 01 Mar 2024 26:00:00 GMT", badParse, untouched, 0}, diff --git a/core/libs/tests/GuestRudpContextQueries.cpp b/core/libs/tests/GuestRudpContextQueries.cpp index 881c31363d..76374d4b86 100644 --- a/core/libs/tests/GuestRudpContextQueries.cpp +++ b/core/libs/tests/GuestRudpContextQueries.cpp @@ -7,8 +7,8 @@ extern "C" { using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); -int APS5_VABI sceRudpEnd(int); -int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); int APS5_VABI sceRudpGetNumberOfPacketsToRead(int); int APS5_VABI sceRudpGetSizeWritable(int); int APS5_VABI sceRudpGetSizeReadable(int); @@ -46,10 +46,10 @@ int main() { Require(sceRudpGetNumberOfPacketsToRead(context) == 0); Require(sceRudpGetSizeWritable(context) == notAcceptable); Require(sceRudpGetSizeReadable(context) == notAcceptable); - Require(sceRudpEnd(context) == 0); + Require(sceRudpTerminate(context) == 0); Require(sceRudpGetNumberOfPacketsToRead(context) == invalidContext); Require(sceRudpGetSizeWritable(context) == invalidContext); Require(sceRudpGetSizeReadable(context) == invalidContext); - Require(sceRudpTerminate() == 0); + Require(sceRudpEnd() == 0); return 0; } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 62a2bd9c8d..1e8ec7ab29 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -8,7 +8,7 @@ extern "C" { int APS5_VABI sceRudpInit_nid_postfix(void*, int); -int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpEnd(); int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); @@ -43,7 +43,7 @@ int main() { std::uint16_t segment[3] = {0, 0, 0x5a5a}; CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); - CHECK(sceRudpTerminate() == 0); + CHECK(sceRudpEnd() == 0); segmentSize = 0x5a5a; CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); diff --git a/core/libs/tests/GuestRudpStatus.cpp b/core/libs/tests/GuestRudpStatus.cpp index 3a6e229e30..ab845fffac 100644 --- a/core/libs/tests/GuestRudpStatus.cpp +++ b/core/libs/tests/GuestRudpStatus.cpp @@ -11,6 +11,8 @@ using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpGetStatus(void*, std::size_t); int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpTerminate(int); +int APS5_VABI sceRudpEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -20,6 +22,7 @@ static void APS5_VABI Handler(int, int, int, void*) {} int main() { constexpr int notInitialized = static_cast(0x80770001); constexpr int alreadyInitialized = static_cast(0x80770002); + constexpr int invalidContextId = static_cast(0x80770003); constexpr int invalidArgument = static_cast(0x80770004); constexpr int memory = static_cast(0x80770007); constexpr std::size_t statusSize = 0xF8; @@ -33,6 +36,8 @@ int main() { Require(sceRudpGetStatus(nullptr, 0) == notInitialized); Require(sceRudpGetStatus(status.data(), statusSize + 1) == notInitialized); Require(status == untouched); + Require(sceRudpEnd() == notInitialized); + Require(sceRudpTerminate(0) == notInitialized); Require(sceRudpInit_nid_postfix(nullptr, sizeof(pool)) == invalidArgument); Require(sceRudpInit_nid_postfix(pool, 0) == invalidArgument); @@ -69,5 +74,19 @@ int main() { Require(sceRudpGetStatus(status.data(), 1) == 0); Require(status[0] == 0); for (std::size_t i = 1; i < status.size(); ++i) Require(status[i] == 0x5a); + + Require(sceRudpTerminate(first) == 0); + Require(sceRudpTerminate(first) == invalidContextId); + Require(sceRudpGetStatus(status.data(), statusSize) == 0); + std::memcpy(&contexts, status.data() + currentContexts, sizeof(contexts)); + Require(contexts == 1); + + Require(sceRudpEnd() == 0); + Require(sceRudpEnd() == notInitialized); + Require(sceRudpTerminate(second) == notInitialized); + Require(sceRudpGetStatus(status.data(), statusSize) == notInitialized); + Require(sceRudpInit_nid_postfix(pool + 8, 0xC58) == 0); + Require(sceRudpTerminate(second) == invalidContextId); + Require(sceRudpEnd() == 0); return 0; } diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp index 8df9b84da5..580d22e892 100644 --- a/core/libs/tests/GuestSaveDataConvert.cpp +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -12,12 +12,15 @@ extern "C" { int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataGetConvertProgress(float*); int APS5_VABI sceSaveDataInitialize3(const void*); +int APS5_VABI sceSaveDataTerminate(); } namespace { constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotInitialized = -2137063423; constexpr int SaveDataErrorNotFound = -2137063416; int failures = 0; @@ -53,16 +56,32 @@ int main() { const auto previous = std::filesystem::current_path(); std::filesystem::current_path(work); + float progress = 0.0f; + Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query before initialize fails with not initialized"); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds"); + Check(progress == 0.0f, "convert progress reports 0.0f initially"); + Check(sceSaveDataGetConvertProgress(nullptr) == SaveDataErrorParameter, "null progress pointer is rejected"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after failed convert"); + Check(progress < 1.0f, "convert progress is below 1.0f after failed convert"); + Check(progress == 0.0f, "convert progress reports 0.0f after failed convert"); Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after successful convert"); + Check(progress == 1.0f, "convert progress reports 1.0f after successful convert"); Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + Check(sceSaveDataGetConvertProgress(&progress) == 0, "get convert progress succeeds after subsequent failed convert"); + Check(progress < 1.0f, "convert progress drops below 1.0f after subsequent failed convert"); + Check(progress == 0.0f, "convert progress reports 0.0f after subsequent failed convert"); for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); } char unterminated[sizeof(SceSaveDataDirName::data)]; std::memset(unterminated, 'a', sizeof(unterminated)); Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + Check(sceSaveDataTerminate() == 0, "SaveData terminates"); + Check(sceSaveDataGetConvertProgress(&progress) == SaveDataErrorNotInitialized, "query after terminate fails with not initialized"); std::filesystem::current_path(previous); std::error_code error; diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index 9cc13cdeed..53d0cc5066 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -12,6 +12,7 @@ extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); 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); @@ -71,6 +72,22 @@ static void SortByKey() { Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond second{}; + second.user_id = 1; + second.dir_name = &pattern; + second.key = 1; + second.order = 1; + std::array secondNames{}; + SaveDataDirNameSearchResult secondResult{}; + secondResult.dir_names = secondNames.data(); + secondResult.dir_names_num = 3; + Require(sceSaveDataDirNameSearch2(&second, &secondResult) == 0, "the second search failed"); + Require(secondResult.hit_num == 3 && secondResult.set_num == 3, "the second search must return all three saves"); + Require(std::string(secondNames[0].data) == "SORT_A" && std::string(secondNames[1].data) == "SORT_C" && std::string(secondNames[2].data) == "SORT_B", "the second search must sort like the first"); + second.key = 6; + Require(sceSaveDataDirNameSearch2(&second, &secondResult) == SaveDataErrorParameter, "the second search must reject an unknown sort key"); SaveDataDirNameSearchCond cond{}; cond.user_id = 1; cond.key = 6; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 583cb51cd3..05c06bd611 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -30,6 +30,7 @@ int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetGetpeername(int, void*, std::uint32_t*); int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); @@ -46,16 +47,25 @@ int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); int APS5_VABI sceNetEpollDestroy(int); int APS5_VABI sceNetEpollAbort(int, int); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); 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); +int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); +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); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); @@ -169,6 +179,24 @@ static void CheckPoolStats() { Require(Failed(sceNetGetMemoryPoolStats(pool, &stats), 9)); } +static void CheckUnboundSocketName() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int socket = sceNetSocket("unbound", family, type, type == 1 ? 6 : 17); + if (socket < 0 && family == 28 && *sceNetErrnoLoc() == 47) continue; + Require(socket >= 0); + const std::uint32_t addressSize = family == 2 ? 16 : 28; + std::array address; + address.fill(0xa5); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(socket, address.data(), &size) == 0 && size == addressSize); + Require(address[0] == addressSize && address[1] == family); + Require(std::all_of(address.begin() + 2, address.begin() + addressSize, [](auto byte) { return byte == 0; })); + Require(sceNetSocketClose(socket) == 0); + } + } +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -225,6 +253,23 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { char head[] = "scatter "; char tail[] = "gather"; @@ -282,6 +327,38 @@ static void CheckMessages(int receiver, int sender, const std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int receiver = sceNetSocket("epoll-receiver", 2, 2, 17); + const int sender = sceNetSocket("epoll-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == address.size()); + const int epoll = sceNetEpollCreate("epoll-writable", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 2; + registration.ident = static_cast(sender); + registration.data.u32 = 41; + Require(sceNetEpollControl(epoll, 1, sender, ®istration) == 0); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 2 && ready.ident == static_cast(sender) && ready.data.u32 == 41); + Require(sceNetEpollControl(epoll, 3, sender, nullptr) == 0); + const char payload[] = "epoll"; + char peeked[sizeof(payload)]{}; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(sceNetRecv(receiver, peeked, sizeof(peeked), 2) == sizeof(payload)); + registration.events = 3; + registration.ident = static_cast(receiver); + Require(sceNetEpollControl(epoll, 1, receiver, ®istration) == 0); + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 3 && ready.ident == static_cast(receiver) && ready.data.u32 == 41); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + static void RequireTimeout(int socket, int option, int expected) { int microseconds = -1; std::uint32_t size = sizeof(microseconds); @@ -345,6 +422,32 @@ static void CheckAddressText(int family, const char* text) { Require(sceNetInetNtop(family, address.data(), nullptr, output.size()) == nullptr && *sceNetErrnoLoc() == 22); } +static void CheckEpollIdent() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int receiver = sceNetSocket("epoll-ident", 2, 2, 17); + const int sender = sceNetSocket("epoll-ident-sender", 2, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == address.size()); + const int epoll = sceNetEpollCreate("epoll-ident", 0); + Require(epoll >= 0); + NetEpollEvent registration{}; + registration.events = 1; + registration.reserved = 0x5a5a5a5a; + registration.ident = 0x123456789abcdef0; + registration.data.u64 = 0x1122334455667788; + Require(sceNetEpollControl(epoll, 1, receiver, ®istration) == 0); + const char payload[] = "ident"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + NetEpollEvent ready{}; + Require(sceNetEpollWait(epoll, &ready, 1, 1000000) == 1); + Require(ready.events == 1 && ready.reserved == 0 && ready.ident == static_cast(receiver)); + Require(ready.data.u64 == 0x1122334455667788); + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + static bool ipv6Unavailable = false; static bool HostIpv6LoopbackUnavailable() { @@ -391,6 +494,79 @@ static void CheckLinkLocalAllNodesIpv6() { Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); } +template +static bool Untouched(const std::array& bytes, std::uint32_t from) { + return std::all_of(bytes.begin() + from, bytes.end(), [](auto byte) { return byte == 0xa5; }); +} + +static void CheckDatagramAddressLengths(int family) { + if (family == 28 && ipv6Unavailable) return; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int receiver = sceNetSocket("length-receiver", family, 2, 17); + const int sender = sceNetSocket("length-sender", family, 2, 17); + Require(receiver >= 0 && sender >= 0); + Require(sceNetBind_nid_postfix(receiver, destination.data(), addressSize) == 0); + std::uint32_t size = destination.size(); + Require(sceNetGetsockname(receiver, destination.data(), &size) == 0 && size == addressSize); + Require(sceNetConnect(sender, destination.data(), addressSize) == 0); + std::array source{}; + size = source.size(); + Require(sceNetGetsockname(sender, source.data(), &size) == 0 && size == addressSize); + const char payload[] = "address length"; + for (const std::uint32_t capacity : {0u, 1u, 2u, addressSize - 1, addressSize, 32u, UINT32_MAX}) { + const auto copied = std::min(capacity, addressSize); + std::array address; + address.fill(0xa5); + size = capacity; + Require(sceNetGetsockname(receiver, address.data(), &size) == 0 && size == copied); + Require(std::memcmp(address.data(), destination.data(), copied) == 0 && Untouched(address, copied)); + char received[sizeof(payload)]{}; + Require(sceNetSend(sender, payload, sizeof(payload), 0) == sizeof(payload)); + address.fill(0xa5); + size = capacity; + Require(sceNetRecvfrom(receiver, received, sizeof(received), 2, address.data(), &size) == sizeof(payload)); + Require(size == copied && std::memcmp(address.data(), source.data(), copied) == 0 && Untouched(address, copied)); + address.fill(0xa5); + NetIovec in[] = {{received, sizeof(received)}}; + NetMsghdr message{address.data(), capacity, in, 1, nullptr, 0, -1}; + Require(sceNetRecvmsg(receiver, &message, 0) == sizeof(payload) && std::memcmp(received, payload, sizeof(payload)) == 0); + Require(message.name_length == copied && std::memcmp(address.data(), source.data(), copied) == 0 && Untouched(address, copied)); + } + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + +static void CheckStreamAddressLengths() { + const int listener = sceNetSocket("length-listener", 2, 1, 6); + Require(listener >= 0); + std::array listening{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, listening.data(), listening.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = listening.size(); + Require(sceNetGetsockname(listener, listening.data(), &size) == 0 && size == 16); + const int client = sceNetSocket("length-client", 2, 1, 6); + Require(client >= 0 && sceNetConnect(client, listening.data(), listening.size()) == 0); + std::array clientAddress{}; + size = clientAddress.size(); + Require(sceNetGetsockname(client, clientAddress.data(), &size) == 0 && size == 16); + std::array peer; + peer.fill(0xa5); + size = 1; + const int accepted = sceNetAccept(listener, peer.data(), &size); + Require(accepted >= 0 && size == 1 && peer[0] == 16 && Untouched(peer, 1)); + for (const std::uint32_t capacity : {0u, 1u, 15u, 16u, 28u, UINT32_MAX}) { + const auto copied = std::min(capacity, std::uint32_t{16}); + peer.fill(0xa5); + size = capacity; + Require(sceNetGetpeername(accepted, peer.data(), &size) == 0 && size == copied); + Require(std::memcmp(peer.data(), clientAddress.data(), copied) == 0 && Untouched(peer, copied)); + } + for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -471,13 +647,16 @@ int main() { Require(in6addr_loopback_nid_postfix[i] == (i == 15 ? 1 : 0)); } Require(sceNetInit_nid_postfix() == 0); + CheckEpollWritable(); CheckPoolStats(); CheckEmptyEpoll(); CheckAddressText(2, "127.0.0.1"); CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + CheckEpollIdent(); CheckLinkLocalAllNodesIpv6(); + CheckUnboundSocketName(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); @@ -519,6 +698,10 @@ int main() { Require(started == 0 || (started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36)); Require(sceNetSocketClose(connecting) == 0); + CheckDatagramAddressLengths(2); + CheckDatagramAddressLengths(28); + CheckStreamAddressLengths(); + const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); NetEpollEvent registration{}; @@ -575,7 +758,7 @@ int main() { const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); - Require(udp_receiver >= 0 && udp_sender >= 0); + Require(udp_receiver >= 0 && udp_sender >= 0 && udp_receiver < 1024 && udp_sender < 1024); address = {16, 2, 0, 0, 127, 0, 0, 1}; Require(sceNetBind_nid_postfix(udp_receiver, address.data(), address.size()) == 0); address_size = address.size(); @@ -618,6 +801,17 @@ int main() { Require(((random_readable[udp_receiver / 64] >> (udp_receiver % 64)) & 1u) == 0); Require(random_urgent[random / 64] == 0); Require(close_nid_postfix(random) == 0); + const int kernel_socket = socket_nid_postfix(2, 2, 0); + Require(kernel_socket >= 0 && kernel_socket < 1024 && kernel_socket != udp_receiver && kernel_socket != udp_sender); + std::array kernel_address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(kernel_socket, kernel_address.data(), kernel_address.size()) == 0); + std::uint64_t mixed_writable[16]{}; + mixed_writable[kernel_socket / 64] |= std::uint64_t{1} << (kernel_socket % 64); + mixed_writable[udp_sender / 64] |= std::uint64_t{1} << (udp_sender % 64); + Require(select_nid_postfix(std::max(kernel_socket, udp_sender) + 1, nullptr, mixed_writable, nullptr, wait_second) == 2); + Require(((mixed_writable[kernel_socket / 64] >> (kernel_socket % 64)) & 1u) != 0); + Require(((mixed_writable[udp_sender / 64] >> (udp_sender % 64)) & 1u) != 0); + Require(close_nid_postfix(kernel_socket) == 0); std::uint64_t stdio_readable[16]{1}; std::uint64_t stdio_writable[16]{6}; Require(select_nid_postfix(3, stdio_readable, stdio_writable, nullptr, poll_now) == 2); @@ -625,6 +819,7 @@ int main() { stdio_readable[0] = 1; Require(select_nid_postfix(1, stdio_readable, nullptr, nullptr, poll_now) == 0 && stdio_readable[0] == 0); CheckReceiveTimeout(udp_receiver, udp_sender, address); + CheckMicrosecondTimeouts(udp_receiver); CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); @@ -643,6 +838,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); @@ -669,6 +873,75 @@ int main() { static_cast(0x80410116) && *sceNetErrnoLoc() == 22); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "guest-sce-net.invalid.", records.data(), 5000000, 1, 0) == static_cast(0x804101E1) && records == resolved); + + std::array plain{}; + plain.fill(0xA5); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0); + Require(plain == resolved); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, nullptr, plain.data(), 5000000, 1, 0) == + static_cast(0x80410116) && *sceNetErrnoLoc() == 22 && plain == resolved); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", nullptr, 5000000, 1, 0) == + static_cast(0x80410116) && *sceNetErrnoLoc() == 22); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "guest-sce-net.invalid", plain.data(), 5000000, 1, 0) == + static_cast(0x804101E1) && plain == resolved); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x804101E1)); + for (const int flags : {0x1, 0x10000}) { + bool threw = false; + try { sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, flags); } + catch (const std::runtime_error&) { threw = true; } + Require(threw && plain == resolved); + } + + const std::array loopback{127, 0, 0, 1}; + char reverse[256]{}; + Require(sceNetResolverAbort(resolver, 0) == 0); + 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); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + const int reverse_result = sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0); + Require(reverse_result == 0 || reverse_result == static_cast(0x804101E1)); + Require(sceNetResolverStartNtoa(resolver, nullptr, ipv4.data(), 5000000, 1, 0) == static_cast(0x80410116)); + ipv4.fill(0xA5); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == static_cast(0x80410104) && + *sceNetErrnoLoc() == 4 && ipv4[0] == 0xA5); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0 && ipv4[0] == 127); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == 0); + Require(sceNetResolverAbort(resolver, 1) == 0); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0 && plain == resolved); + Require(sceNetResolverAbort(resolver, 2) == 0); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == 0 && records == resolved); + std::memset(reverse, 0x5A, sizeof(reverse)); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && reverse[0] == 0x5A); + Require(sceNetResolverGetError(resolver, &resolver_error) == 0 && resolver_error == static_cast(0x80410104)); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == reverse_result); + Require(sceNetResolverAbort(resolver, 3) == 0); + Require(sceNetResolverStartAton(resolver, loopback.data(), reverse, sizeof(reverse), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4); + Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == + static_cast(0x80410104) && *sceNetErrnoLoc() == 4 && records == resolved); + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + for (const int flags : {0x4, -1}) { + bool threw = false; + try { sceNetResolverAbort(resolver, flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + Require(sceNetResolverStartNtoa(resolver, "localhost", ipv4.data(), 5000000, 1, 0) == 0); + + const int other = sceNetResolverCreate("guest-sce-net-other", 0, 0); + Require(other >= 0 && other != resolver); + Require(sceNetResolverAbort(other, 3) == 0); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == 0); + Require(sceNetResolverDestroy(other) == 0); + Require(sceNetResolverAbort(other, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); + Require(sceNetResolverAbort(-1, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); Require(sceNetResolverGetError(resolver, nullptr) == static_cast(0x80410116) && *sceNetErrnoLoc() == 22); Require(sceNetResolverDestroy(resolver) == 0); resolver_error = -1; @@ -676,6 +949,9 @@ int main() { *sceNetErrnoLoc() == 9 && resolver_error == -1); Require(sceNetResolverStartNtoaMultipleRecordsEx(resolver, "localhost", records.data(), 5000000, 1, 0) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9 && records == resolved); + Require(sceNetResolverStartNtoaMultipleRecords(resolver, "localhost", plain.data(), 5000000, 1, 0) == + static_cast(0x80410109) && *sceNetErrnoLoc() == 9 && plain == resolved); + Require(sceNetResolverAbort(resolver, 1) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); std::array ipv6{}; Require(sceNetInetPton(28, "::1", ipv6.data()) == 1); @@ -689,6 +965,34 @@ int main() { int state = -1; Require(sceNetCtlGetState(&state) == 0); Require(state == 0 || state == 3); + Require(sceNetCtlGetIfStat(nullptr) == static_cast(0x80412107)); + { + const char* counterPath = "guest_sce_net_byte_counter.txt"; + std::FILE* counter = std::fopen(counterPath, "w"); + Require(counter != nullptr); + Require(std::fputs("1099511627776", counter) >= 0); + Require(std::fclose(counter) == 0); + Require(NetCtlReadByteCounter_nid_no_patch(counterPath) == 1099511627776ull); + Require(std::remove(counterPath) == 0); + bool missing = false; + try { + NetCtlReadByteCounter_nid_no_patch(counterPath); + } catch (const std::runtime_error&) { + missing = true; + } + Require(missing); + } + NetCtlIfStat first{}; + first.device = 7; + first.reserved[7] = 7; + const int statResult = sceNetCtlGetIfStat(&first); + Require(statResult == (state == 3 ? 0 : static_cast(0x80412108))); + if (statResult == 0) { + Require(first.device == 0 && first.reserved[7] == 0); + NetCtlIfStat second{}; + Require(sceNetCtlGetIfStat(&second) == 0); + Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); + } 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/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index bd6cd8d500..f5eddd3d84 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -25,6 +25,12 @@ extern "C" { int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI close_nid_postfix(int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int* APS5_VABI __error_nid_postfix(); @@ -106,6 +112,44 @@ int main() { Require(*__error_nid_postfix() == GuestEinval); } + { + int pipe[2]{-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && receiver < GuestDescriptorLimit && sender >= 0 && sender < GuestDescriptorLimit); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + const int highest = receiver > pipe[0] ? receiver : pipe[0]; + GuestDescriptorSet idle{}; + idle.Add(receiver); + idle.Add(pipe[0]); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, idle.words.data(), nullptr, nullptr, &none) == 0); + Require(!idle.Has(receiver) && !idle.Has(pipe[0])); + const char byte = 'm'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + Require(write_nid_postfix(pipe[1], &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestTimeval limit{2, 0}; + const auto start = std::chrono::steady_clock::now(); + int ready = 0; + while (ready < 2 && std::chrono::steady_clock::now() - start < std::chrono::seconds(2)) { + read = {}; + read.Add(receiver); + read.Add(pipe[0]); + ready = select_nid_postfix(highest + 1, read.words.data(), nullptr, nullptr, &limit); + } + Require(ready == 2 && read.Has(receiver) && read.Has(pipe[0])); + char buffer[1]{}; + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0) == 1 && buffer[0] == byte); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1 && buffer[0] == byte); + for (const int descriptor : {receiver, sender, pipe[0], pipe[1]}) Require(close_nid_postfix(descriptor) == 0); + } + Require(close_nid_postfix(reader) == 0); Require(close_nid_postfix(writer) == 0); 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/GuestSetrlimit.cpp b/core/libs/tests/GuestSetrlimit.cpp new file mode 100644 index 0000000000..383b48d06f --- /dev/null +++ b/core/libs/tests/GuestSetrlimit.cpp @@ -0,0 +1,74 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +struct ResourceLimit { + std::int64_t rlim_cur; + std::int64_t rlim_max; +}; + +extern "C" { +int APS5_VABI getrlimit_nid_postfix(int, ResourceLimit*); +int APS5_VABI setrlimit_nid_postfix(int, const ResourceLimit*); +int* APS5_VABI __error_nid_postfix(); +} + +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_RLIMIT_DATA = 2; +static constexpr int GUEST_RLIMIT_NOFILE = 8; +static constexpr int GUEST_RLIM_NLIMITS = 15; +static constexpr std::int64_t GUEST_RLIM_INFINITY = std::numeric_limits::max(); + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Throws(int resource, const ResourceLimit& limit) { + try { + setrlimit_nid_postfix(resource, &limit); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void RequireError(int resource, const ResourceLimit* limit, int error) { + *__error_nid_postfix() = 0; + Require(setrlimit_nid_postfix(resource, limit) == -1); + Require(*__error_nid_postfix() == error); +} + +int main() { + const ResourceLimit infinity{GUEST_RLIM_INFINITY, GUEST_RLIM_INFINITY}; + RequireError(-1, nullptr, GUEST_EFAULT); + RequireError(GUEST_RLIM_NLIMITS, nullptr, GUEST_EFAULT); + RequireError(-1, &infinity, GUEST_EINVAL); + RequireError(GUEST_RLIM_NLIMITS, &infinity, GUEST_EINVAL); + + ResourceLimit current{}; + Require(getrlimit_nid_postfix(GUEST_RLIMIT_DATA, ¤t) == 0); + *__error_nid_postfix() = 13; + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, ¤t) == 0); + Require(*__error_nid_postfix() == 13); + + if (current.rlim_cur > 0) { + const ResourceLimit changed{current.rlim_cur - 1, current.rlim_max}; + Require(Throws(GUEST_RLIMIT_DATA, changed)); + } + const ResourceLimit softAboveHard{GUEST_RLIM_INFINITY, current.rlim_max}; + if (current.rlim_cur == current.rlim_max) + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, &softAboveHard) == 0); + else + Require(Throws(GUEST_RLIMIT_DATA, softAboveHard)); + + const ResourceLimit negative{-1, -2}; + if (current.rlim_cur == GUEST_RLIM_INFINITY && current.rlim_max == GUEST_RLIM_INFINITY) + Require(setrlimit_nid_postfix(GUEST_RLIMIT_DATA, &negative) == 0); + else + Require(Throws(GUEST_RLIMIT_DATA, negative)); + + Require(Throws(GUEST_RLIMIT_NOFILE, infinity)); + return 0; +} diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 400e2f5a73..698b357e1e 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); int APS5_VABI sceShareCaptureScreenshot(const void* param, std::int32_t* req_id); int APS5_VABI sceShareCaptureVideoClip(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureVideoClipExtended(const void* extended_param, std::int32_t* req_id); int APS5_VABI sceShareGetCurrentStatus(std::uint32_t feature_flag, void* status); int APS5_VABI sceShareOpenMenuForContent(const void* content_id); int APS5_VABI sceShareGetRunningStatus(std::uint32_t* status); @@ -24,18 +25,21 @@ int main() { constexpr std::int32_t notSupported = static_cast(0x81960007); std::uint8_t param[64]{}; - std::int32_t reqId = 7; - Require(sceShareCaptureScreenshotExtended(param, &reqId) == notSupported); - Require(reqId == -1); - - reqId = 7; - Require(sceShareCaptureScreenshotExtended(nullptr, &reqId) == notSupported); - Require(reqId == -1); - - Require(sceShareCaptureScreenshotExtended(param, nullptr) == notSupported); - Require(sceShareCaptureScreenshotExtended(nullptr, nullptr) == notSupported); - using Capture = int (APS5_VABI*)(const void*, std::int32_t*); + std::int32_t reqId = 7; + for (Capture capture : {sceShareCaptureScreenshotExtended, sceShareCaptureVideoClipExtended}) { + reqId = 7; + Require(capture(param, &reqId) == notSupported); + Require(reqId == -1); + + reqId = 7; + Require(capture(nullptr, &reqId) == notSupported); + Require(reqId == -1); + + Require(capture(param, nullptr) == notSupported); + Require(capture(nullptr, nullptr) == notSupported); + } + for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { reqId = 7; Require(capture(param, &reqId) == notSupported); diff --git a/core/libs/tests/GuestSharePlay.cpp b/core/libs/tests/GuestSharePlay.cpp index ba62ad7da0..fe2819c276 100644 --- a/core/libs/tests/GuestSharePlay.cpp +++ b/core/libs/tests/GuestSharePlay.cpp @@ -9,6 +9,7 @@ extern "C" { int APS5_VABI sceSharePlayInitialize(void*, std::size_t); int APS5_VABI sceSharePlayTerminate(void); int APS5_VABI sceSharePlayGetCurrentConnectionInfoA(SharePlayConnectionInfoA*); +int APS5_VABI sceSharePlayProhibitController(int); } static void Require(bool value) { if (!value) std::abort(); } @@ -38,4 +39,9 @@ int main() { Require(info.hostOnlineId.data[0] == 0 && info.visitorOnlineId.data[0] == 0 && info.hostOnlineId.pad[2] == 0); Require(info.hostUserId == -1 && info.visitorUserId == -1); Require(sceSharePlayGetCurrentConnectionInfoA(nullptr) == static_cast(0x810E0001)); + Require(sceSharePlayProhibitController(1) == 0); + Require(sceSharePlayProhibitController(1) == 0); + Require(sceSharePlayProhibitController(0) == 0); + Require(Throws([] { sceSharePlayProhibitController(2); })); + Require(Throws([] { sceSharePlayProhibitController(-1); })); } diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index b0368a76e6..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; @@ -60,10 +61,8 @@ int main() { Require(pthread_kill_nid_postfix(nullptr, 15) == 3); Require(pthread_kill_nid_postfix(nullptr, 128) == 3); Require(pthread_kill_nid_postfix(nullptr, 129) == 22); - bool threw = false; - try { pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 15); } - catch (const std::runtime_error&) { threw = true; } - Require(threw && received == 0); + Require(pthread_kill_nid_postfix(reinterpret_cast(std::uintptr_t{0x10}), 15) == 3); + Require(received == 0); Require(signal_nid_postfix(15, ignore) != invalid); received = 0; Require(raise_nid_postfix(15) == 0 && received == 0); @@ -202,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 83d93847a8..03a9a237b8 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -1,9 +1,10 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include -#include -#include +#include struct GuestSignalSet { std::uint32_t bits[4]; }; @@ -11,6 +12,20 @@ struct GuestTimespec { std::int64_t seconds; std::int64_t nanoseconds; }; +struct GuestSiginfo { + std::int32_t signo; + std::int32_t error; + std::int32_t code; + std::int32_t pid; + std::uint32_t uid; + std::int32_t status; + std::uintptr_t address; + std::int32_t value; + std::int32_t valueTail; + std::int64_t band; + std::int32_t spare[7]; + std::int32_t tail; +}; using Handler = void (APS5_VABI *)(int); extern "C" { int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); @@ -19,63 +34,137 @@ int APS5_VABI sigwait_nid_postfix(const GuestSignalSet*, int*); int APS5_VABI pause_nid_postfix(void); Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI getpid_nid_postfix(void); +Pthread APS5_VABI scePthreadSelf(void); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_kill_nid_postfix(Pthread thread, int guest); +int* APS5_VABI __error_nid_postfix(); } volatile int received = 0; void APS5_VABI Callback(int value) { received = value; } static void Require(bool value) { if (!value) std::abort(); } -template -static bool ReportsNotImplemented(TCall call, const char* message) { - try { - call(); - } catch (const std::runtime_error& error) { - return std::string(error.what()) == message; - } - return false; +static void RequireUntouched(const GuestSiginfo& info) { + const GuestSiginfo empty{}; + Require(std::memcmp(&info, &empty, sizeof(info)) == 0); } -static bool SigtimedwaitReportsNotImplemented(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { - return ReportsNotImplemented([&] { sigtimedwait_nid_postfix(set, info, timeout); }, "sigtimedwait: waiting for a pending signal not implemented"); +struct Sender { + Pthread target = nullptr; + int guest = 0; + int result = -1; +}; +static void* APS5_VABI Send(void* arg) { + auto& sender = *static_cast(arg); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + sender.result = pthread_kill_nid_postfix(sender.target, sender.guest); + return nullptr; } -static bool SigwaitinfoReportsNotImplemented(const GuestSignalSet* set, void* info) { - return ReportsNotImplemented([&] { sigwaitinfo_nid_postfix(set, info); }, "sigwaitinfo: waiting for a pending signal not implemented"); +static void* APS5_VABI SendWithoutDelay(void* arg) { + auto& sender = *static_cast(arg); + sender.result = pthread_kill_nid_postfix(sender.target, sender.guest); + return nullptr; } -static bool SigwaitReportsNotImplemented(const GuestSignalSet* set, int* taken) { - return ReportsNotImplemented([&] { sigwait_nid_postfix(set, taken); }, "sigwait: waiting for a pending signal not implemented"); +static Pthread StartSender(Sender& sender) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Send, &sender, nullptr) == 0); + return thread; +} +static Pthread StartSenderWithoutDelay(Sender& sender) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SendWithoutDelay, &sender, nullptr) == 0); + return thread; +} +static void JoinSender(Pthread thread, const Sender& sender) { + Require(scePthreadJoin(thread, nullptr) == 0 && sender.result == 0); } int main() { - const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; const GuestSignalSet empty{{0, 0, 0, 0}}; + const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; - unsigned char info[64] = {}; - const unsigned char untouched[64] = {}; const GuestTimespec poll{0, 0}; - const GuestTimespec second{1, 0}; - Require(SigtimedwaitReportsNotImplemented(&pipe, info, &poll)); - Require(SigtimedwaitReportsNotImplemented(&pipe, nullptr, &second)); - Require(SigtimedwaitReportsNotImplemented(&pipe, info, nullptr)); - - Require(SigwaitinfoReportsNotImplemented(&pipe, info)); - Require(SigwaitinfoReportsNotImplemented(&pipe, nullptr)); - Require(SigwaitinfoReportsNotImplemented(&empty, info)); - Require(std::memcmp(info, untouched, sizeof(info)) == 0); - + const GuestTimespec expired{0, 1000000000}; + GuestSiginfo info{}; int taken = -1; - Require(SigwaitReportsNotImplemented(&pipe, &taken)); - Require(SigwaitReportsNotImplemented(&empty, &taken)); - Require(taken == -1); - - Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(nullptr, &info, &poll) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&empty, &info, &poll) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + 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); + RequireUntouched(info); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == -1 && *__error_nid_postfix() == 35); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&interrupt, &info, &expired) == -1 && *__error_nid_postfix() == 22); + RequireUntouched(info); GuestSignalSet previous{}; Require(signal_nid_postfix(2, Callback) != reinterpret_cast(static_cast(-1))); Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + received = 0; Require(raise_nid_postfix(2) == 0 && received == 0); - Require(SigwaitReportsNotImplemented(&interrupt, &taken) && taken == -1); - Require(SigwaitinfoReportsNotImplemented(&interrupt, info)); - Require(std::memcmp(info, untouched, sizeof(info)) == 0); + Require(sigwait_nid_postfix(&interrupt, &taken) == 0 && taken == 2 && received == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigtimedwait_nid_postfix(&interrupt, &info, &poll) == 2 && received == 0); + Require(info.signo == 2 && info.error == 0 && info.code == 0 && info.pid == getpid_nid_postfix() && info.uid == 0); + Require(info.status == 0 && info.address == 0 && info.value == 0 && info.band == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&interrupt, &info) == 2 && received == 0); + Require(info.signo == 2 && info.code == 0 && info.pid == getpid_nid_postfix()); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&interrupt, nullptr) == 2 && received == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(sigtimedwait_nid_postfix(&interrupt, nullptr, nullptr) == 2 && received == 0); GuestSignalSet current{}; Require(_sigprocmask_nid_postfix(1, nullptr, ¤t) == 0 && current.bits[0] == (previous.bits[0] | interrupt.bits[0])); Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + received = 0; Require(raise_nid_postfix(2) == 0 && received == 2); - Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); + + Require(_sigprocmask_nid_postfix(1, &pipe, &previous) == 0); + received = 0; + Require(raise_nid_postfix(13) == 0 && received == 0); + Require(sigwaitinfo_nid_postfix(&pipe, &info) == 13 && info.signo == 13 && info.pid == getpid_nid_postfix()); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + + const Pthread self = scePthreadSelf(); + *__error_nid_postfix() = 0; + Require(pthread_kill_nid_postfix(nullptr, 13) == 3 && *__error_nid_postfix() == 0); + Require(pthread_kill_nid_postfix(self, 129) == 22); + Sender sender{self, 13, -1}; + Pthread senderThread = StartSender(sender); + Require(sigwaitinfo_nid_postfix(&pipe, &info) == 13); + JoinSender(senderThread, sender); + Require(info.signo == 13 && info.code == 0 && info.pid == getpid_nid_postfix()); + senderThread = StartSender(sender); + Require(sigtimedwait_nid_postfix(&pipe, nullptr, nullptr) == 13); + JoinSender(senderThread, sender); + Require(signal_nid_postfix(15, Callback) != reinterpret_cast(static_cast(-1))); + senderThread = StartSender(sender = Sender{self, 15, -1}); + received = 0; + *__error_nid_postfix() = 0; + Require(pause_nid_postfix() == -1 && *__error_nid_postfix() == 4 && received == 15); + JoinSender(senderThread, sender); + Require(signal_nid_postfix(15, nullptr) == Callback); + + // The senders above sleep 50 ms first, so the signal always arrives while the wait is + // already blocked. A raise landing between the check of the pending set and the wait + // used to be lost, and the wait then never returned, so raise with no delay instead, + // many times. + Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + for (int round = 0; round < 500; ++round) { + Sender tight{self, 2, -1}; + const Pthread senderThread = StartSenderWithoutDelay(tight); + Require(sigwait_nid_postfix(&interrupt, &taken) == 0 && taken == 2); + JoinSender(senderThread, tight); + } + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); } diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp new file mode 100644 index 0000000000..e8340085ae --- /dev/null +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -0,0 +1,189 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct GuestDescriptorSet { + std::array words{}; + + void Add(int descriptor) { + words[descriptor / 64] |= (std::uint64_t{1} << (descriptor % 64)); + } + + bool Has(int descriptor) const { + return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; + } +}; + +struct PollDescriptor { + int descriptor; + short events; + short revents; +}; + +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 getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI open_nid_postfix(const char*, int, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket descriptor check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int DescriptorSetSize = 1024; +constexpr int Inet = 2; +constexpr int Datagram = 2; +constexpr short In = 0x1; +constexpr int KernelBadDescriptor = static_cast(0x80020009u); + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static int Socket() { + const int descriptor = socket_nid_postfix(Inet, Datagram, 0); + Require(descriptor >= 0 && descriptor < DescriptorSetSize); + return descriptor; +} + +static int BoundSocket(std::array& address) { + const int descriptor = Socket(); + address = {16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &size) == 0); + return descriptor; +} + +static int ConnectedSocket(const std::array& address) { + const int descriptor = Socket(); + Require(connect_nid_postfix(descriptor, address.data(), address.size()) == 0); + return descriptor; +} + +static void WaitReadable(int descriptor) { + GuestDescriptorSet read{}; + read.Add(descriptor); + GuestTimeval limit{2, 0}; + Require(select_nid_postfix(descriptor + 1, read.words.data(), nullptr, nullptr, &limit) == 1); + Require(read.Has(descriptor)); +} + +static void SelectSocketAndFile() { + std::array address{}; + const int receiver = BoundSocket(address); + const int sender = ConnectedSocket(address); + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0 && file < DescriptorSetSize && file != receiver && file != sender); + const int highest = std::max({receiver, sender, file}); + { + GuestDescriptorSet read{}; + read.Add(receiver); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(receiver + 1, read.words.data(), nullptr, nullptr, &none) == 0); + Require(!read.Has(receiver)); + } + { + GuestDescriptorSet read{}; + read.Add(receiver); + read.Add(file); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), nullptr, nullptr, &none) == 1); + Require(read.Has(file) && !read.Has(receiver)); + } + const char byte = 'd'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + WaitReadable(receiver); + { + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + read.Add(receiver); + read.Add(file); + write.Add(sender); + GuestTimeval none{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), nullptr, &none) == 3); + Require(read.Has(receiver) && read.Has(file) && write.Has(sender)); + } + char received = 0; + Require(recv_nid_postfix(receiver, &received, 1, 0) == 1 && received == byte); + for (const int descriptor : {receiver, sender, file}) Require(close_nid_postfix(descriptor) == 0); +} + +static void CloseReleasesNumber() { + const int socket = Socket(); + Require(close_nid_postfix(socket) == 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(socket, &status) == -1 && *__error_nid_postfix() == 9); + std::vector files; + while (files.size() < 8 && (files.empty() || files.back() != socket)) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + files.push_back(file); + } + Require(files.back() == socket); + unsigned char bytes[4]{}; + Require(read_nid_postfix(socket, bytes, sizeof(bytes)) == sizeof(bytes)); + PollDescriptor entry{socket, In, 0}; + Require(Throws([&] { poll_nid_postfix(&entry, 1, 0); })); + for (const int file : files) Require(close_nid_postfix(file) == 0); + const int kernelClosed = Socket(); + Require(sceKernelClose(kernelClosed) == 0); + Require(sceKernelClose(kernelClosed) == KernelBadDescriptor); + entry = {kernelClosed, In, 0}; + Require(poll_nid_postfix(&entry, 1, 0) == 1 && entry.revents == 0x20); +} + +static void DuplicateKeepsSocket() { + std::array address{}; + const int receiver = BoundSocket(address); + const int sender = ConnectedSocket(address); + const int duplicate = dup_nid_postfix(receiver); + Require(duplicate >= 0 && duplicate < DescriptorSetSize && duplicate != receiver && duplicate != sender); + Require(close_nid_postfix(receiver) == 0); + const char byte = 'u'; + Require(send_nid_postfix(sender, &byte, 1, 0) == 1); + WaitReadable(duplicate); + char received = 0; + Require(recv_nid_postfix(duplicate, &received, 1, 0) == 1 && received == byte); + Require(close_nid_postfix(duplicate) == 0); + Require(close_nid_postfix(sender) == 0); +} + +int main() { + SelectSocketAndFile(); + CloseReleasesNumber(); + DuplicateKeepsSocket(); + return 0; +} diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 9624ce8069..ff2b0b5d27 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -232,7 +232,7 @@ int main(int argc, char** argv) { std::memcmp(buffer, payload, sizeof(payload)) == 0, "receive into watched sender outputs"); Require(*length == senderAddress.size() && std::memcmp(address, senderAddress.data(), *length) == 0, "watched sender outputs are correct"); - const auto expectedPages = static_cast(((mask & 1) != 0) + ((mask & 2) != 0)) * (page / 4096); + const auto expectedPages = static_cast(((mask & 1) != 0) + ((mask & 2) != 0)); Require(collectOutputs() == expectedPages, "each watched sender output is tracked"); Require(collectOutputs() == 0, "sender output writes cleared"); if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); diff --git a/core/libs/tests/GuestSocketPair.cpp b/core/libs/tests/GuestSocketPair.cpp index 561645111f..3ab148efd5 100644 --- a/core/libs/tests/GuestSocketPair.cpp +++ b/core/libs/tests/GuestSocketPair.cpp @@ -51,7 +51,7 @@ constexpr int CloseOnExec = 0x10000000; constexpr int NonBlocking = 0x20000000; constexpr int NoSignal = 0x20000; constexpr int Peek = 2; -constexpr int FirstSocket = 0x10000000; +constexpr int DescriptorSetSize = 1024; constexpr short In = 0x1; constexpr short Out = 0x4; constexpr short Hup = 0x10; @@ -90,7 +90,7 @@ static bool Unnamed(int descriptor, int (APS5_VABI *query)(int, void*, std::uint int main() { int pair[2] = {-1, -1}; Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); - Require(pair[0] >= FirstSocket && pair[1] >= FirstSocket && pair[0] != pair[1]); + Require(pair[0] >= 0 && pair[0] < DescriptorSetSize && pair[1] >= 0 && pair[1] < DescriptorSetSize && pair[0] != pair[1]); Require(Exchange(pair[0], pair[1], "one way")); Require(Exchange(pair[1], pair[0], "other way")); Require(Unnamed(pair[0], getsockname_nid_postfix) && Unnamed(pair[1], getpeername_nid_postfix)); diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index a8bed92584..4d89668ab6 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -12,6 +12,7 @@ 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); } struct SslMemoryPoolStats { @@ -50,6 +51,7 @@ 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); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index b3cb7d20a9..0e18fd8fbd 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -13,7 +13,9 @@ #include #include #include -#ifndef _WIN32 +#ifdef _WIN32 +#include +#else #include #endif extern "C" { @@ -267,18 +269,28 @@ static void CheckRewind() { rewind_nid_postfix(&stream); Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0 && fgetc_nid_postfix(&stream) == 'a'); stream.Close(); -#ifndef _WIN32 int pipeEnds[2]; +#ifdef _WIN32 + Require(_pipe(pipeEnds, 4096, _O_BINARY) == 0 && _write(pipeEnds[1], "pq", 2) == 2 && _close(pipeEnds[1]) == 0); + FileStream pipe(_fdopen(pipeEnds[0], "rb")); +#else Require(::pipe(pipeEnds) == 0); - Require(::write(pipeEnds[1], "p", 1) == 1); + Require(::write(pipeEnds[1], "pq", 2) == 2); ::close(pipeEnds[1]); FileStream pipe(::fdopen(pipeEnds[0], "r")); - Require(fgetc_nid_postfix(&pipe) == 'p' && fgetc_nid_postfix(&pipe) == EOF && feof_nid_postfix(&pipe) != 0); +#endif + Require(fgetc_nid_postfix(&pipe) == 'p'); + for (const int origin : {SEEK_SET, SEEK_CUR, SEEK_END}) { + *__error_nid_postfix() = 0; + Require(fseeko_nid_postfix(&pipe, 0, origin) == -1 && *__error_nid_postfix() == 29); + } + *__error_nid_postfix() = 0; + Require(ftello_nid_postfix(&pipe) == -1 && *__error_nid_postfix() == 29 && ferror_nid_postfix(&pipe) == 0); + Require(fgetc_nid_postfix(&pipe) == 'q' && fgetc_nid_postfix(&pipe) == EOF && feof_nid_postfix(&pipe) != 0); *__error_nid_postfix() = 0; rewind_nid_postfix(&pipe); Require(*__error_nid_postfix() == 29 && feof_nid_postfix(&pipe) == 0 && ferror_nid_postfix(&pipe) == 0); pipe.Close(); -#endif } static void CheckFlushWhileReading() { @@ -392,6 +404,31 @@ static void CheckDescriptorModes(std::FILE* original) { #endif } +static void CheckDescriptorAppend(std::FILE* original) { + std::string expected = "retained"; + for (const char* mode : {"a", "ab", "a+", "ae"}) { + const int descriptor = DuplicateDescriptor(original); +#ifdef _WIN32 + Require(descriptor >= 0 && _lseek(descriptor, 2, SEEK_SET) == 2); +#else + Require(descriptor >= 0 && ::lseek(descriptor, 2, SEEK_SET) == 2); +#endif + auto* wrapped = fdopen_nid_postfix(descriptor, mode); + Require(wrapped != nullptr && fileno_nid_postfix(wrapped) == descriptor); + Require(fputc_nid_postfix('+', wrapped) == '+' && fflush_nid_postfix(wrapped) == 0); + Require(fseek_nid_postfix(wrapped, 0, SEEK_SET) == 0 && fputc_nid_postfix('+', wrapped) == '+'); + Require(fclose_nid_postfix(wrapped) == 0); + expected += "++"; + const int reader = DuplicateDescriptor(original); + Require(reader >= 0); + auto* written = fdopen_nid_postfix(reader, "r"); + Require(written != nullptr && fseek_nid_postfix(written, 0, SEEK_SET) == 0); + char contents[32]{}; + Require(fgets_nid_postfix(contents, sizeof(contents), written) == contents && expected == contents); + Require(fclose_nid_postfix(written) == 0); + } +} + int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); @@ -421,6 +458,7 @@ int main() { Require(::close(descriptor) == -1); #endif CheckDescriptorModes(original); + CheckDescriptorAppend(original); Require(std::fclose(original) == 0); char stringOutput[256]; std::memset(stringOutput, '!', sizeof(stringOutput)); diff --git a/core/libs/tests/GuestStatvfs.cpp b/core/libs/tests/GuestStatvfs.cpp new file mode 100644 index 0000000000..a71484b6f8 --- /dev/null +++ b/core/libs/tests/GuestStatvfs.cpp @@ -0,0 +1,157 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +extern "C" int APS5_VABI statvfs_nid_postfix(const char*, void*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "statvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireVolume(const char* path, const std::filesystem::path& native) { + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(statvfs_nid_postfix(path, output.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + const auto* fields = output.data() + 1; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] == std::filesystem::space(native).capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +static void RequireError(const char* path, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + const int result = statvfs_nid_postfix(path, output.data()); + if (result != -1) + std::fprintf(stderr, "path %s: expected failure, got %d\n", path ? path : "(null)", result); + Require(result == -1); + if (*__error_nid_postfix() != error) + std::fprintf(stderr, "path %s: expected errno %d, got %d\n", path ? path : "(null)", error, *__error_nid_postfix()); + Require(*__error_nid_postfix() == error && output == saved); +} + +static std::size_t OpenHandles() { +#ifdef _WIN32 + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); + return count; +#else + return static_cast(std::distance(std::filesystem::directory_iterator("/proc/self/fd"), + std::filesystem::directory_iterator{})); +#endif +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-statvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "file.txt"; + { + std::ofstream stream(file, std::ios::binary); + stream << "unchanged"; + Require(static_cast(stream)); + } + AddPathAlias_nid_no_patch("/anyps5-statvfs-test", root.string().c_str()); + RequireVolume("/anyps5-statvfs-test", root); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + RequireVolume("/anyps5-statvfs-test/", root); + RequireError(nullptr, 14); + RequireError("", 2); + RequireError("/anyps5-statvfs-test/missing", 2); + RequireError("/anyps5-statvfs-test/file.txt/child", 20); + RequireError("/anyps5-statvfs-test/file.txt/", 20); + RequireError("/anyps5-statvfs-test/file.txt/.", 20); + RequireError("/anyps5-statvfs-test/file.txt/../file.txt", 20); + RequireError("/anyps5-statvfs-test/missing/../file.txt", 2); + BlockPathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + RequireError("/anyps5-statvfs-unavailable", 2); + AddPathAlias_nid_no_patch("/anyps5-statvfs-unavailable", root.string().c_str()); + RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + + const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt"); + { + std::ofstream stream(unicodeFile); + stream << "unicode path"; + Require(static_cast(stream)); + } + RequireVolume("/anyps5-statvfs-test/\xE6\x97\xA5.txt", unicodeFile); + Require(std::filesystem::remove(unicodeFile)); +#ifdef _WIN32 + const HANDLE exclusive = CreateFileW(file.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(exclusive != INVALID_HANDLE_VALUE); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(CloseHandle(exclusive) != 0); +#else + Require(::chmod(file.c_str(), 0) == 0); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(::chmod(file.c_str(), 0600) == 0); + const auto fifo = root / "fifo"; + Require(::mkfifo(fifo.c_str(), 0600) == 0); + RequireVolume("/anyps5-statvfs-test/fifo", fifo); + const auto socketPath = root / "socket"; + const int socket = ::socket(AF_UNIX, SOCK_STREAM, 0); + Require(socket >= 0); + sockaddr_un address{}; + address.sun_family = AF_UNIX; + const auto name = socketPath.string(); + Require(name.size() < sizeof(address.sun_path)); + std::memcpy(address.sun_path, name.c_str(), name.size() + 1); + Require(::bind(socket, reinterpret_cast(&address), sizeof(address)) == 0); + RequireVolume("/anyps5-statvfs-test/socket", socketPath); + Require(::close(socket) == 0); + Require(std::filesystem::remove(socketPath)); + Require(std::filesystem::remove(fifo)); + std::filesystem::create_symlink(file.filename(), root / "link"); + RequireVolume("/anyps5-statvfs-test/link", file); + Require(std::filesystem::remove(root / "link")); + std::filesystem::create_symlink("missing", root / "dangling"); + RequireError("/anyps5-statvfs-test/dangling", 2); + Require(std::filesystem::remove(root / "dangling")); +#endif + const auto before = OpenHandles(); + for (int i = 0; i != 32; ++i) { + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + } + Require(OpenHandles() == before); + { + std::ifstream stream(file, std::ios::binary); + std::string text((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + Require(text == "unchanged"); + } + RemovePathAlias_nid_no_patch("/anyps5-statvfs-test"); + Require(std::filesystem::remove(file)); + Require(std::filesystem::remove(root)); +} diff --git a/core/libs/tests/GuestStrftime.cpp b/core/libs/tests/GuestStrftime.cpp new file mode 100644 index 0000000000..eef399eda0 --- /dev/null +++ b/core/libs/tests/GuestStrftime.cpp @@ -0,0 +1,95 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" +#include +#include +#include + +extern "C" { +std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*); +char* APS5_VABI asctime_nid_postfix(const GuestTm*); +} + +namespace { + +GuestTm MakeTime(int year, int hour, int wday, int yday, int isdst) { + GuestTm time{}; + time.tm_sec = 5; + time.tm_min = 4; + time.tm_hour = hour; + time.tm_mday = 2; + time.tm_mon = 0; + time.tm_year = year; + time.tm_wday = wday; + time.tm_yday = yday; + time.tm_isdst = isdst; + time.tm_gmtoff = 19800; + time.tm_zone = "IST"; + return time; +} + +} + +int main() { + struct Case { + const char* name; + int year; + int hour; + int wday; + int yday; + int isdst; + const char* format; + const char* expected; + }; + constexpr Case cases[] = { + {"alternative", 126, 3, 5, 1, 0, "%Ea %OX %OD %EF %EEY", "Fri 03:04:05 01/02/26 2026-01-02 EY"}, + {"padding", 126, 3, 5, 1, 0, "%-d|%_d|%0e|%-k|%_+|%--d", "2| 2|02|3|Fri Jan 2 03:04:05 IST 2026|-d"}, + {"zone", 126, 3, 5, 1, 0, "%z %Z %0z", "+0530 IST +0530"}, + {"year 899", -1001, 3, 5, 1, 0, "%Y %C %y %G %g %F", "0899 08 99 0899 99 0899-01-02"}, + {"year -1", -1901, 3, 5, 1, 0, "%Y %C %y %c", "-001 -0 01 Fri Jan 2 03:04:05 -001"}, + {"year 12345", 10445, 3, 5, 1, 0, "%Y %C %y %D %v", "12345 123 45 01/02/45 2-Jan-12345"}, + {"out of range", 126, 100, 8, 400, 0, "%I %l %u %W %U %j %a %p", "04 4 8 57 57 401 ? PM"}, + {"unknown dst", 126, 3, 5, 1, -1, "[%z][%Z]", "[][IST]"}, + {"literals", 126, 3, 5, 1, 0, "%q %% %", "q % %"}, + }; + bool correct = true; + for (const auto& test : cases) { + const GuestTm time = MakeTime(test.year, test.hour, test.wday, test.yday, test.isdst); + char text[128]; + const std::size_t length = strftime_nid_postfix(text, sizeof(text), test.format, &time); + if (length != std::strlen(test.expected) || std::strcmp(text, test.expected) != 0) { + std::fprintf(stderr, "strftime %s (\"%s\"): expected \"%s\", received \"%s\" (%zu)\n", + test.name, test.format, test.expected, length == 0 ? "" : text, length); + correct = false; + } + } + const GuestTm now = MakeTime(126, 3, 5, 1, 0); + char text[32]; + if (strftime_nid_postfix(text, 10, "%Y-%m-%d", &now) != 0 || strftime_nid_postfix(text, 11, "%Y-%m-%d", &now) != 10) { + std::fprintf(stderr, "strftime does not return 0 when the result and its terminator do not fit\n"); + correct = false; + } + if (strftime_nid_postfix(text, sizeof(text), nullptr, &now) == 0 || std::strcmp(text, "Fri Jan 2 03:04:05 2026") != 0) { + std::fprintf(stderr, "strftime with a null format does not format %%c\n"); + correct = false; + } + struct AsctimeCase { + int year; + const char* expected; + }; + constexpr AsctimeCase asctimeCases[] = { + {126, "Fri Jan 2 03:04:05 2026\n"}, + {-1001, "Fri Jan 2 03:04:05 0899\n"}, + {-1901, "Fri Jan 2 03:04:05 -001\n"}, + {10445, "Fri Jan 2 03:04:05 12345\n"}, + {-2147483647 - 1, "Fri Jan 2 03:04:05 -2147481748\n"}, + }; + for (const auto& test : asctimeCases) { + const GuestTm time = MakeTime(test.year, 3, 5, 1, 0); + const char* result = asctime_nid_postfix(&time); + if (std::strcmp(result, test.expected) != 0) { + std::fprintf(stderr, "asctime of tm_year %d: expected \"%s\", received \"%s\"\n", test.year, test.expected, result); + correct = false; + } + } + return correct ? 0 : 1; +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 6fb9a35ac2..a1c5feafc3 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -342,11 +342,32 @@ int main() { char* firstState = nullptr; char* secondState = nullptr; Require(std::strcmp(strtok_r_nid_postfix(first, ",", &firstState), "a") == 0); + Require(firstState == first + 3); Require(std::strcmp(strtok_r_nid_postfix(second, ":", &secondState), "x") == 0); + Require(secondState == second + 2); Require(std::strcmp(strtok_r_nid_postfix(nullptr, ",", &firstState), "b") == 0); + Require(firstState == first + 6); Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); + if (firstState != nullptr) std::fputs("strtok_r retained the state after exhausting delimiters\n", stderr); + Require(firstState == nullptr); Require(strtok_r_nid_postfix(nullptr, ",", &firstState) == nullptr); Require(std::strcmp(strtok_r_nid_postfix(nullptr, "", &secondState), "y") == 0); + Require(secondState == nullptr); + char tokenEnd[8] = "last"; + char* finishedState = first; + *__error_nid_postfix() = 77; + Require(strtok_r_nid_postfix(tokenEnd, ",", &finishedState) == tokenEnd && finishedState == nullptr); + tokenEnd[4] = 'x'; + tokenEnd[5] = '\0'; + Require(strtok_r_nid_postfix(nullptr, ",", &finishedState) == nullptr && finishedState == nullptr); + for (const char* input : {"", ",", ",,,"}) { + char noTokens[8]; + std::strcpy(noTokens, input); + finishedState = first; + Require(strtok_r_nid_postfix(noTokens, ",", &finishedState) == nullptr && finishedState == nullptr); + Require(strtok_r_nid_postfix(nullptr, ",", &finishedState) == nullptr); + } + Require(*__error_nid_postfix() == 77); char hostTokens[] = "host:next"; Require(std::strcmp(std::strtok(hostTokens, ":"), "host") == 0); char guestTokens[] = ",one,,two:three"; diff --git a/core/libs/tests/GuestSyslog.cpp b/core/libs/tests/GuestSyslog.cpp index ad8adb881d..0d55e1a440 100644 --- a/core/libs/tests/GuestSyslog.cpp +++ b/core/libs/tests/GuestSyslog.cpp @@ -2,15 +2,19 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#include #else #include #endif extern "C" { void APS5_VABI syslog_nid_postfix(int, const char*, ...); +void APS5_VABI openlog_nid_postfix(const char*, int, int); +void APS5_VABI closelog_nid_postfix(); int APS5_VABI setlogmask_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -60,6 +64,15 @@ int main() { Require(setlogmask_nid_postfix(0) == 0xff); syslog_nid_postfix(7, "mask shared across threads"); syslog_nid_postfix(7, "wide %ls %d %S", u"guest", 5, u"text"); + openlog_nid_postfix("worker", 1, 16 << 3); + syslog_nid_postfix(6, "configured default facility"); + openlog_nid_postfix(nullptr, 1, 0); + syslog_nid_postfix(6, "null identity keeps tag"); + openlog_nid_postfix("daemon", 0, 24 << 3); + syslog_nid_postfix(16 | 3, "explicit facility"); + openlog_nid_postfix("closing", 1, 0); + closelog_nid_postfix(); + syslog_nid_postfix(6, "identity cleared, options and facility retained"); std::fflush(stderr); #ifdef _WIN32 Require(::_dup2(saved, ::_fileno(stderr)) == 0 && ::_close(saved) == 0); @@ -86,6 +99,18 @@ int main() { "[syslog:22] level 6\n" "[syslog:15] mask shared across threads\n" "[syslog:15] wide guest 5 text\n"; - Require(length == std::strlen(expected) && std::strcmp(text, expected) == 0); + const std::string prefix = expected; + const std::string pid = std::to_string( +#ifdef _WIN32 + ::_getpid() +#else + ::getpid() +#endif + ); + const std::string configured = "[syslog:134] worker[" + pid + "]: configured default facility\n" + "[syslog:134] worker[" + pid + "]: null identity keeps tag\n" + "[syslog:19] daemon: explicit facility\n" + "[syslog:198] [" + pid + "]identity cleared, options and facility retained\n"; + Require(length == prefix.size() + configured.size() && std::string(text, length) == prefix + configured); Require(std::fclose(captured) == 0); } diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index a5383a863e..4c7f86455c 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -43,8 +43,10 @@ constexpr std::uint16_t kUltModuleId = 0x0007; constexpr int kInvalidValue = static_cast(0x805A1000); constexpr int kModuleUnloaded = static_cast(0x805A1001); constexpr std::uint32_t kInternalFlag = 0x80000000u; +constexpr std::uint16_t kAcmModuleId = 0x010b; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; constexpr std::uint16_t kVoiceChatModuleId = 0x0114; +constexpr std::uint16_t kPsmlModuleId = 0x0137; constexpr std::uint32_t kAgcModuleId = 0x80000094; #ifdef __linux__ @@ -132,6 +134,13 @@ int main() { Require(sceSysmoduleUnloadModule(kUltModuleId) == 0); Require(sceSysmoduleIsLoaded(kUltModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kAcmModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kAcmModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kAcmModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAcmModuleId) == 0); + Require(sceSysmoduleUnloadModule(kAcmModuleId) == 0); + Require(sceSysmoduleIsLoaded(kAcmModuleId) == kModuleUnloaded); + constexpr std::uint16_t kShareModuleId = 0x010f; Require(sceSysmoduleIsLoaded(kShareModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModule(kShareModuleId) == 0); @@ -156,6 +165,13 @@ int main() { Require(sceSysmoduleUnloadModule(kVoiceChatModuleId) == 0); Require(sceSysmoduleIsLoaded(kVoiceChatModuleId) == kModuleUnloaded); + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModule(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModule(kPsmlModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == 0); + Require(sceSysmoduleUnloadModule(kPsmlModuleId) == 0); + Require(sceSysmoduleIsLoaded(kPsmlModuleId) == kModuleUnloaded); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 069ef27671..1ad0f5c6f7 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -86,6 +86,26 @@ static void CheckPhysicalMemorySysctl() { 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); } +#if defined(__x86_64__) || defined(__i386__) +static void CheckModelSysctl() { + const int mib[] = {6, 2}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length > 1); + const std::size_t size = length; + char model[128] = {}; + length = sizeof(model); + Require(sysctlbyname_nid_postfix("hw.model", model, &length, nullptr, 0) == 0 && length == size); + Require(std::strlen(model) + 1 == size && model[0] != ' '); + Require(*__error_nid_postfix() == 13); + char truncated[4] = {}; + length = sizeof(truncated); + Require(sysctl_nid_postfix(mib, 2, truncated, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == sizeof(truncated) && std::memcmp(truncated, model, sizeof(truncated)) == 0); + length = sizeof(model); + Require(sysctlbyname_nid_postfix("hw.model", model, &length, model, size) == -1 && *__error_nid_postfix() == 1); +} +#endif static void CheckMachdepCachelineSizeSysctl() { *__error_nid_postfix() = 13; int value = 0x5a5a5a5a; @@ -232,6 +252,9 @@ static void CheckArandomSysctl() { int main() { CheckProcessorCountSysctl(); CheckPhysicalMemorySysctl(); +#if defined(__x86_64__) || defined(__i386__) + CheckModelSysctl(); +#endif CheckMachdepCachelineSizeSysctl(); CheckPathnameSysctl(); CheckSystemSwVersion(); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 6dc172ac0d..56c4146351 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -1,14 +1,24 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include #include #include +#include +#include +#include #include #include +#include +#include #ifdef _WIN32 #include +#else +#include +#include #endif 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 catchReturnFromMain_nid_postfix(int); using GuestQuickExitCallback = void (APS5_VABI*)(); @@ -16,9 +26,11 @@ extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { bool cleaned = false; +bool finalized = false; void Cleanup() { cleaned = true; } +void APS5_VABI Finalize(void*) { finalized = true; } void VerifyExit() { - if (!cleaned) std::_Exit(1); + if (!cleaned || !finalized) std::_Exit(1); std::puts("Guest shutdown and atexit completed"); } int quickOrder = 0; @@ -33,6 +45,79 @@ void UnexpectedCleanup() { std::_Exit(3); } #ifdef _WIN32 void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); } #endif +constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; +constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE"; +void SetVariable(const char* name, const char* value) { +#ifdef _WIN32 + Require(SetEnvironmentVariableA(name, value) != 0); + Require(_putenv_s(name, value) == 0); +#else + Require(setenv(name, value, 1) == 0); +#endif +} +std::filesystem::path OwnExecutable() { +#ifdef _WIN32 + std::wstring executable(32768, L'\0'); + executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); + return executable; +#else + return std::filesystem::read_symlink("/proc/self/exe"); +#endif +} +void AppendMarker(const std::string& line) { + const char* marker = std::getenv(RelaunchMarker); + Require(marker != nullptr); + std::ofstream(marker, std::ios::app) << line << '\n'; +} +void MainShutdown() { AppendMarker("shutdown"); } +void SecondaryShutdown() { AppendMarker("secondary shutdown"); } +int RunRelaunchingChild(const std::filesystem::path& self) { +#ifdef _WIN32 + std::wstring commandLine = L"\"" + self.wstring() + L"\" --relaunch"; + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(self.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + WaitForSingleObject(process.hProcess, INFINITE); + DWORD code = 1; + GetExitCodeProcess(process.hProcess, &code); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return static_cast(code); +#else + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + execl(self.c_str(), self.c_str(), "--relaunch", static_cast(nullptr)); + std::_Exit(127); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +#endif +} +int DriveRelaunch() { + const auto self = OwnExecutable(); + const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); + std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing); + 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); + 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)); + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + for (int attempt = 0; attempt < 100 && std::filesystem::exists(secondary) && !std::filesystem::remove(secondary, ignored); ++attempt) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(text == "shutdown\narguments: --secondary|two words|quote\"d\\|\nsecondary shutdown\nrelaunched\n"); + std::puts("Guest relaunch completed"); + return 0; +} } int main(int argc, char** argv) { if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) { @@ -55,16 +140,49 @@ int main(int argc, char** argv) { quick_exit_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { + return DriveRelaunch(); + } + if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) { + LibcRegisterShutdown_nid_postfix(MainShutdown); + const char* const arguments[] = {"--secondary", "two words", "quote\"d\\", "", nullptr}; + sceSystemServiceLoadExec("/app0/picker/picker.bin", arguments); + return 2; + } + if (argc > 1 && std::strcmp(argv[1], "--secondary") == 0) { + Require(OwnExecutable().filename().string().starts_with("picker.picker.bin")); + std::string received = "arguments:"; + for (int index = 1; index < argc; ++index) received += std::string(index == 1 ? " " : "|") + argv[index]; + AppendMarker(received); + LibcRegisterShutdown_nid_postfix(SecondaryShutdown); + const char* const arguments[] = {"--relaunched", nullptr}; + sceSystemServiceLoadExec("/app0/eboot.bin", arguments); + return 2; + } + if (argc > 1 && std::strcmp(argv[1], "--relaunched") == 0) { + const char* main = std::getenv(MainExecutable); + Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); + AppendMarker("relaunched"); + return 0; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); + Require(__cxa_atexit_nid_postfix(Finalize, nullptr, nullptr) == 0); Require(std::atexit(VerifyExit) == 0); sceSystemServiceLoadExec("exit", nullptr); return 2; } Require(sceSystemServiceLoadExec(nullptr, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceLoadExec("", nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - bool rejected = false; - try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { + bool rejected = false; + try { sceSystemServiceLoadExec(path, nullptr); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + } +#ifdef _WIN32 + Require(GetEnvironmentVariableA("APS5_EBOOT", nullptr, 0) == 0); +#else + Require(std::getenv("APS5_EBOOT") == nullptr); +#endif } diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 13b062c85f..e4e181e204 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -42,6 +42,8 @@ extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceDisableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceReenableMediaPlay(void); extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, void* param); +extern "C" int APS5_VABI sceSystemServiceShowControllerSettings(void); +extern "C" int APS5_VABI sceSystemServiceOpenChallengeActivity(void); int main() { Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); @@ -57,6 +59,8 @@ int main() { Require(sceSystemServiceDisableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReenableMediaPlay() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceShowControllerSettings() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceOpenChallengeActivity() == SYSTEM_SERVICE_OK); Require(sceSystemServiceLaunchWebBrowser("http://127.0.0.1:8780/video?v=0", nullptr) == browserUnavailable); unsigned char browserParam[64]; std::memset(browserParam, 0x5a, sizeof(browserParam)); diff --git a/core/libs/tests/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 3b399294de..2b211538ed 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); @@ -18,6 +20,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI shutdown_nid_postfix(int, int); std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -76,9 +79,69 @@ static void ListenersShareAPortAcrossFamilies() { Require(close_nid_postfix(inet) == 0); } +constexpr int GetFlags = 3; +constexpr int SetFlags = 4; +constexpr int NonBlockingFlag = 4; + +static int AcceptWithin(int listener) { + for (int attempt = 0; attempt < 200; ++attempt) { + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + if (accepted >= 0) return accepted; + Require(*__error_nid_postfix() == 35); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return -1; +} + +static void AcceptedSocketsFollowListenerBlocking() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0 && listen_nid_postfix(listener, 4) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + + const int firstClient = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(firstClient, address.data(), address.size()) == 0); + const int blocking = accept_nid_postfix(listener, nullptr, nullptr); + Require(blocking >= 0 && (fcntl_nid_postfix(blocking, GetFlags) & NonBlockingFlag) == 0); + + Require(fcntl_nid_postfix(listener, SetFlags, NonBlockingFlag) == 0); + const int secondClient = socket_nid_postfix(2, 1, 0); + Require(connect_nid_postfix(secondClient, address.data(), address.size()) == 0); + const int inherited = AcceptWithin(listener); + Require(inherited >= 0 && (fcntl_nid_postfix(inherited, GetFlags) & NonBlockingFlag) != 0); + char byte; + Require(recv_nid_postfix(inherited, &byte, 1, 0) == -1 && *__error_nid_postfix() == 35); + Require(fcntl_nid_postfix(inherited, SetFlags, 0) == 0 && (fcntl_nid_postfix(inherited, GetFlags) & NonBlockingFlag) == 0); + Require((fcntl_nid_postfix(listener, GetFlags) & NonBlockingFlag) != 0); + + for (const int descriptor : {firstClient, secondClient, blocking, inherited, listener}) Require(close_nid_postfix(descriptor) == 0); +} + +static void UnboundSocketNameIsZero() { + for (const int family : {2, 28}) { + for (const int type : {1, 2}) { + const int descriptor = socket_nid_postfix(family, type, 0); + Require(descriptor >= 0); + const std::uint32_t expected = family == 2 ? 16 : 28; + std::array name; + name.fill(0xaa); + std::uint32_t size = name.size(); + Require(getsockname_nid_postfix(descriptor, name.data(), &size) == 0); + Require(size == expected && name[0] == expected && name[1] == family); + for (std::uint32_t index = 2; index < expected; ++index) Require(name[index] == 0); + Require(name[expected] == 0xaa); + Require(close_nid_postfix(descriptor) == 0); + } + } +} + int main() { StreamReceiveReportsNoSender(); ListenersShareAPortAcrossFamilies(); + AcceptedSocketsFollowListenerBlocking(); + UnboundSocketNameIsZero(); const int listener = socket_nid_postfix(2, 1, 6); Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/core/libs/tests/GuestTerminal.cpp b/core/libs/tests/GuestTerminal.cpp index 19653acc33..ad27b96a30 100644 --- a/core/libs/tests/GuestTerminal.cpp +++ b/core/libs/tests/GuestTerminal.cpp @@ -5,14 +5,23 @@ #include #include #include +#include #include extern "C" { int APS5_VABI tcgetattr_nid_postfix(int, void*); int APS5_VABI tcsetattr_nid_postfix(int, int, const void*); +int APS5_VABI tcsendbreak_nid_postfix(int, int); +int APS5_VABI tcdrain_nid_postfix(int); +int APS5_VABI tcflush_nid_postfix(int, int); +int APS5_VABI tcflow_nid_postfix(int, int); +int APS5_VABI tcgetpgrp_nid_postfix(int); +int APS5_VABI tcsetpgrp_nid_postfix(int, int); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } @@ -34,9 +43,45 @@ constexpr int BadDescriptor = 9; constexpr int InvalidArgument = 22; constexpr int BadAddress = 14; constexpr int AddressNotAvailable = 49; +constexpr int OperationNotSupported = 45; static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } +static void RequireControlFails(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(Fails(tcsendbreak_nid_postfix(descriptor, 0), error)); + *__error_nid_postfix() = 0; + Require(Fails(tcsendbreak_nid_postfix(descriptor, 5), error)); + *__error_nid_postfix() = 0; + Require(Fails(tcdrain_nid_postfix(descriptor), error)); + for (const int queue : {1, 2, 3}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflush_nid_postfix(descriptor, queue), error)); + } + for (const int action : {1, 2, 3, 4}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflow_nid_postfix(descriptor, action), error)); + } +} + +static void RequireProcessGroupFails(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(Fails(tcgetpgrp_nid_postfix(descriptor), error)); + for (const int group : {0, 1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcsetpgrp_nid_postfix(descriptor, group), error)); + } +} + +template static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static void RequireNotTerminal(int descriptor) { unsigned char attributes[44]; std::memset(attributes, 0xaa, sizeof(attributes)); @@ -49,6 +94,7 @@ static void RequireNotTerminal(int descriptor) { *__error_nid_postfix() = 0; Require(Fails(tcsetattr_nid_postfix(descriptor, action, attributes), NotTerminal)); } + RequireControlFails(descriptor, NotTerminal); } int main() { @@ -56,14 +102,19 @@ int main() { const int file = open_nid_postfix(name.c_str(), 0x202, 0644); Require(file >= 0); RequireNotTerminal(file); - RequireNotTerminal(0); - RequireNotTerminal(1); - RequireNotTerminal(2); + RequireProcessGroupFails(file, NotTerminal); + for (const int stream : {0, 1, 2}) { + RequireNotTerminal(stream); + RequireProcessGroupFails(stream, NotTerminal); + } int ends[2] = {-1, -1}; Require(pipe_nid_postfix(ends) == 0); - RequireNotTerminal(ends[0]); - RequireNotTerminal(ends[1]); + for (const int end : ends) { + RequireNotTerminal(end); + Require(Throws([end] { tcgetpgrp_nid_postfix(end); })); + Require(Throws([end] { tcsetpgrp_nid_postfix(end, 0); })); + } const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); @@ -74,6 +125,27 @@ int main() { Require(Fails(tcgetattr_nid_postfix(socket, probe), AddressNotAvailable)); Require(std::memcmp(probe, untouched, sizeof(probe)) == 0); Require(Fails(tcsetattr_nid_postfix(socket, Flush, probe), AddressNotAvailable)); + RequireControlFails(socket, AddressNotAvailable); + RequireProcessGroupFails(socket, AddressNotAvailable); + + int pair[2] = {-1, -1}; + Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); + for (const int end : pair) { + Require(Fails(tcgetattr_nid_postfix(end, probe), OperationNotSupported)); + RequireControlFails(end, OperationNotSupported); + RequireProcessGroupFails(end, OperationNotSupported); + } + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + + const auto directoryName = name + "-directory"; + Require(mkdir_nid_postfix(directoryName.c_str(), 0755) == 0); + const int directory = open_nid_postfix(directoryName.c_str(), 0x00020000, 0); + Require(directory >= 0); + RequireNotTerminal(directory); + RequireProcessGroupFails(directory, NotTerminal); + Require(close_nid_postfix(directory) == 0); + RequireControlFails(directory, BadDescriptor); + std::filesystem::remove(directoryName); unsigned char attributes[44]{}; Require(Fails(tcsetattr_nid_postfix(file, 3, attributes), InvalidArgument)); @@ -84,14 +156,32 @@ int main() { Require(Fails(tcsetattr_nid_postfix(-1, Now, nullptr), BadAddress)); Require(Fails(tcgetattr_nid_postfix(-1, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(-1, Now, attributes), BadDescriptor)); + for (const int descriptor : {file, -1}) { + for (const int queue : {0, 4, -1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflush_nid_postfix(descriptor, queue), InvalidArgument)); + } + for (const int action : {0, 5, -1}) { + *__error_nid_postfix() = 0; + Require(Fails(tcflow_nid_postfix(descriptor, action), InvalidArgument)); + } + } + RequireControlFails(-1, BadDescriptor); + RequireProcessGroupFails(-1, BadDescriptor); Require(close_nid_postfix(socket) == 0); Require(Fails(tcgetattr_nid_postfix(socket, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(socket, Now, attributes), BadDescriptor)); + RequireControlFails(socket, BadDescriptor); + RequireProcessGroupFails(socket, BadDescriptor); Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); + RequireControlFails(ends[0], BadDescriptor); + RequireProcessGroupFails(ends[0], BadDescriptor); Require(close_nid_postfix(file) == 0); Require(Fails(tcgetattr_nid_postfix(file, attributes), BadDescriptor)); Require(Fails(tcsetattr_nid_postfix(file, Flush, attributes), BadDescriptor)); + RequireControlFails(file, BadDescriptor); + RequireProcessGroupFails(file, BadDescriptor); std::filesystem::remove(name); return 0; } diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp new file mode 100644 index 0000000000..4694194aeb --- /dev/null +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -0,0 +1,76 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void ReadThermalSensors_nid_no_patch(const char* root, int celsius[3]); +int APS5_VABI sceKernelGetCpuTemperature(int* outCelsius); +int APS5_VABI sceKernelGetSocSensorTemperature(int sensorId, int* outCelsius); +} + +static void Require(bool value) { + if (!value) { + std::fprintf(stderr, "thermal sensors check failed\n"); + std::abort(); + } +} + +int main() { + const auto root = std::filesystem::path("anyps5-thermal-sensors-test"); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "hwmon" / "hwmon0"); + std::filesystem::create_directories(root / "hwmon" / "hwmon1"); + std::filesystem::create_directories(root / "thermal" / "thermal_zone0"); + std::ofstream(root / "hwmon" / "hwmon0" / "name") << "k10temp\n"; + std::ofstream(root / "hwmon" / "hwmon0" / "temp1_input") << "45200\n"; + std::ofstream(root / "hwmon" / "hwmon1" / "name") << "nvme\n"; + std::ofstream(root / "hwmon" / "hwmon1" / "temp1_input") << "39800\n"; + std::ofstream(root / "thermal" / "thermal_zone0" / "type") << "acpitz\n"; + std::ofstream(root / "thermal" / "thermal_zone0" / "temp") << "28000\n"; + + int celsius[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch(root.string().c_str(), celsius); + Require(celsius[0] == 45 && celsius[1] == 28 && celsius[2] == 39); + + int missing[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch((root / "absent").string().c_str(), missing); + Require(missing[0] == -1 && missing[1] == -1 && missing[2] == -1); + ReadThermalSensors_nid_no_patch(nullptr, missing); + Require(missing[0] == -1); + + const auto skipped = std::filesystem::path("anyps5-thermal-sensors-skip"); + std::filesystem::remove_all(skipped); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonB"); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonA"); + std::filesystem::create_directories(skipped / "hwmon" / "hwmonC"); + std::ofstream(skipped / "hwmon" / "hwmonA" / "name") << "amdgpu\n"; + std::ofstream(skipped / "hwmon" / "hwmonA" / "temp1_input") << "55000\n"; + std::ofstream(skipped / "hwmon" / "hwmonB" / "name") << "BAT0\n"; + std::ofstream(skipped / "hwmon" / "hwmonB" / "temp1_input") << "31600\n"; + std::ofstream(skipped / "hwmon" / "hwmonC" / "name") << "iwlwifi_1\n"; + std::ofstream(skipped / "hwmon" / "hwmonC" / "temp1_input") << "57000\n"; + int fallback[3] = {0, 0, 0}; + ReadThermalSensors_nid_no_patch(skipped.string().c_str(), fallback); + Require(fallback[0] == 55 && fallback[1] == -1 && fallback[2] == -1); + std::filesystem::remove_all(skipped); + + int value = 0; + Require(sceKernelGetCpuTemperature(nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetSocSensorTemperature(0, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Require(sceKernelGetSocSensorTemperature(-1, &value) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetSocSensorTemperature(3, &value) == SCE_KERNEL_ERROR_EINVAL); + + int sentinel = -12345; + Require(sceKernelGetSocSensorTemperature(15, &sentinel) == SCE_KERNEL_ERROR_EINVAL && sentinel == -12345); + int probe = -12345; + const int result = sceKernelGetSocSensorTemperature(0, &probe); + Require(result == 0 ? (probe >= 0 && probe <= 130) : (result == SCE_KERNEL_ERROR_EINVAL && probe == -12345)); + + std::filesystem::remove_all(root); + return 0; +} diff --git a/core/libs/tests/GuestThreadCallbackAbi.cpp b/core/libs/tests/GuestThreadCallbackAbi.cpp index a496f88512..4fe7aa04ca 100644 --- a/core/libs/tests/GuestThreadCallbackAbi.cpp +++ b/core/libs/tests/GuestThreadCallbackAbi.cpp @@ -14,6 +14,7 @@ using GuestDestructor = void(APS5_VABI *)(void*); extern "C" { int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(GuestDestructor, void*, void*); +int APS5_VABI __cxa_thread_atexit_nid_postfix(GuestDestructor, void*, void*); int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(GuestDestructor, void*, void*); } @@ -24,7 +25,7 @@ struct Call { void* object; }; -std::array calls{}; +std::array calls{}; std::atomic callCount{0}; std::atomic registrationFailed{false}; @@ -37,6 +38,7 @@ void Record(int id, void* object) { void APS5_VABI FirstDestructor(void* object) { Record(1, object); } void APS5_VABI SecondDestructor(void* object) { Record(2, object); } void APS5_VABI ThirdDestructor(void* object) { Record(3, object); } +void APS5_VABI FourthDestructor(void* object) { Record(4, object); } void Fail(const char* message) { std::fprintf(stderr, "%s\n", message); } @@ -55,13 +57,11 @@ int main() { } void* directDso = module; -#ifdef _WIN32 - directDso = nullptr; -#endif int firstObject = 1; int secondObject = 2; int thirdObject = 3; + int fourthObject = 4; std::thread worker([&] { if (__cxa_thread_atexit_impl_nid_postfix(FirstDestructor, &firstObject, directDso) != 0) registrationFailed.store(true); @@ -69,6 +69,8 @@ int main() { registrationFailed.store(true); if (LibcInternalExtCxaThreadAtexit_nid_postfix(ThirdDestructor, &thirdObject, module) != 0) registrationFailed.store(true); + if (__cxa_thread_atexit_nid_postfix(FourthDestructor, &fourthObject, directDso) != 0) + registrationFailed.store(true); }); worker.join(); @@ -84,7 +86,7 @@ int main() { Fail("thread exit did not run each registered destructor once"); return 1; } - const std::array expected = {{{3, &thirdObject}, {2, &secondObject}, {1, &firstObject}}}; + const std::array expected = {{{4, &fourthObject}, {3, &thirdObject}, {2, &secondObject}, {1, &firstObject}}}; for (std::size_t index = 0; index < expected.size(); ++index) { if (calls[index].id != expected[index].id || calls[index].object != expected[index].object) { std::fprintf(stderr, "thread destructor %d received object %p; expected %p\n", calls[index].id, diff --git a/core/libs/tests/GuestThreadSanitizer.cpp b/core/libs/tests/GuestThreadSanitizer.cpp new file mode 100644 index 0000000000..4403a31564 --- /dev/null +++ b/core/libs/tests/GuestThreadSanitizer.cpp @@ -0,0 +1,9 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" int APS5_VABI sceKernelIsThreadSanitizerEnabled(void); + +int main() { + if (sceKernelIsThreadSanitizerEnabled() != 0) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index bdf1486d4f..879c664511 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -252,6 +252,50 @@ static void CheckLiteralConversions() { Require(buffer[0] == 'x' && buffer[1] == 'y', "strftime wrote to a zero-size buffer"); } +static void CheckLocalRange() { + constexpr std::array timers{-1, -7200, -7201, -86400, -2208988800, -12622780801, -62135596800, 32535216000, 40000000000, 253402300799}; + for (const std::int64_t timer : timers) { + GuestTm local{}; + *__error_nid_postfix() = 0; + Require(localtime_s_nid_postfix(&timer, &local) == &local && local.tm_gmtoff == 7200, "Local conversion rejected a time outside 1970 to 3000"); + Require(Equal(local, UtcTime(timer + 7200)), "Local conversion outside 1970 to 3000 has the wrong calendar fields"); + Require(local.tm_zone != nullptr && std::strlen(local.tm_zone) > 0, "Local conversion outside 1970 to 3000 did not set tm_zone"); + GuestTm normalized = local; + normalized.tm_wday = -1; + normalized.tm_yday = -1; + Require(mktime_nid_postfix(&normalized) == timer && Equal(normalized, local), "mktime did not invert localtime outside 1970 to 3000"); + Require(*__error_nid_postfix() == 0, "A local conversion outside 1970 to 3000 set errno"); + const GuestTm* shared = localtime_nid_postfix(&timer); + Require(shared != nullptr && Equal(*shared, local), "localtime rejected a time outside 1970 to 3000"); + Require(ctime_nid_postfix(&timer)[0] != '?', "ctime rejected a time outside 1970 to 3000"); + } + GuestTm beforeEpoch{}; + beforeEpoch.tm_year = 70; + beforeEpoch.tm_mon = -1; + beforeEpoch.tm_mday = 1; + beforeEpoch.tm_isdst = -1; + Require(mktime_nid_postfix(&beforeEpoch) == -2685600 && beforeEpoch.tm_year == 69 && beforeEpoch.tm_mon == 11 && beforeEpoch.tm_mday == 1 + && beforeEpoch.tm_wday == 1 && beforeEpoch.tm_yday == 334, "mktime did not normalize a month before 1970"); + GuestTm afterLimit{}; + afterLimit.tm_year = 1200; + afterLimit.tm_mon = 17; + afterLimit.tm_mday = 15; + afterLimit.tm_hour = 12; + afterLimit.tm_isdst = -1; + const std::int64_t late = mktime_nid_postfix(&afterLimit); + Require(late == 35705181600 && afterLimit.tm_year == 1201 && afterLimit.tm_mon == 5 && afterLimit.tm_mday == 15 && afterLimit.tm_hour == 12 + && Equal(afterLimit, UtcTime(late + 7200)), "mktime did not normalize a month after 3000"); + GuestTm farDay{}; + farDay.tm_year = 70; + farDay.tm_mday = -36500; + farDay.tm_isdst = -1; + const std::int64_t early = mktime_nid_postfix(&farDay); + Require(early == -3153693600 && farDay.tm_mday >= 1 && Equal(farDay, UtcTime(early + 7200)), "mktime did not normalize a day before 1970"); + const std::int64_t overflow = std::numeric_limits::min(); + GuestTm scratch{}; + Require(localtime_s_nid_postfix(&overflow, &scratch) == nullptr, "Unrepresentable negative local time was accepted"); +} + int main() { #ifdef _WIN32 _putenv_s("TZ", "UTC-2"); @@ -273,6 +317,18 @@ int main() { const std::int64_t underflow = std::numeric_limits::min(); GuestTm scratch{}; Require(gmtime_s_nid_postfix(&underflow, &scratch) == nullptr, "Unrepresentable negative time was accepted"); + const std::int64_t overflow = std::numeric_limits::max(); + for (const std::int64_t* timer : {&underflow, &overflow}) { + *__error_nid_postfix() = 1234; + Require(gmtime_s_nid_postfix(timer, &scratch) == nullptr && *__error_nid_postfix() == 1234, + "gmtime_s of an unrepresentable time changed errno"); + Require(gmtime_nid_postfix(timer) == nullptr && *__error_nid_postfix() == 1234, + "gmtime of an unrepresentable time changed errno"); + Require(localtime_s_nid_postfix(timer, &scratch) == nullptr && *__error_nid_postfix() == 1234, + "localtime_s of an unrepresentable time changed errno"); + Require(localtime_nid_postfix(timer) == nullptr && *__error_nid_postfix() == 1234, + "localtime of an unrepresentable time changed errno"); + } CheckAsctime(UtcTime(0), "Thu Jan 1 00:00:00 1970\n"); CheckAsctime(UtcTime(-2208988801), "Sun Dec 31 23:59:59 1899\n"); @@ -328,6 +384,7 @@ int main() { && std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm"); CheckFreeBsdConversions(); + CheckLocalRange(); CheckLiteralConversions(); CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date"); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 5b4dc26948..c1b4db8b80 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include extern "C" { @@ -14,6 +15,7 @@ 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 bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); @@ -122,6 +124,12 @@ int main() { int option = 0; std::uint32_t optionSize = sizeof(option); Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); + for (const std::int64_t wide : {0, 1}) { + Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &wide, sizeof(wide)) == 0); + optionSize = sizeof(option); + Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && (option != 0) == (wide != 0)); + } + Require(setsockopt_nid_postfix(sender, 0xffff, 0x20, &enabled, 2) == -1 && *__error_nid_postfix() == 22); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); const int unconnected = socket_nid_postfix(2, 2, 0); @@ -204,6 +212,45 @@ int main() { Require(ioctl_nid_postfix(receiver, 0x4004667f, &queued) == -1); Require(_ioctl_nid_postfix(receiver, 0x4004667f, &queued) == -1 && *__error_nid_postfix() == 9); Require(close_nid_postfix(sender) == 0); + const int peer = socket_nid_postfix(2, 2, 0); + const int other = socket_nid_postfix(2, 2, 0); + const int dissolved = socket_nid_postfix(2, 2, 0); + 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(bind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + Require(bind_nid_postfix(dissolved, dissolvedAddress.data(), dissolvedAddress.size()) == 0); + size = peerAddress.size(); + Require(getsockname_nid_postfix(peer, peerAddress.data(), &size) == 0); + size = dissolvedAddress.size(); + Require(getsockname_nid_postfix(dissolved, dissolvedAddress.data(), &size) == 0); + Require(connect_nid_postfix(dissolved, peerAddress.data(), peerAddress.size()) == 0); + std::array peerName{}; + size = peerName.size(); + Require(getpeername_nid_postfix(dissolved, peerName.data(), &size) == 0 && peerName[2] == peerAddress[2] && peerName[3] == peerAddress[3]); + const std::array unspecified{16, 0}; + Require(connect_nid_postfix(dissolved, unspecified.data(), unspecified.size()) == -1 && *__error_nid_postfix() == 47); + size = peerName.size(); + Require(getpeername_nid_postfix(dissolved, peerName.data(), &size) == -1 && *__error_nid_postfix() == 57); + std::array localName{}; + size = localName.size(); + Require(getsockname_nid_postfix(dissolved, localName.data(), &size) == 0); + Require(localName[2] == dissolvedAddress[2] && localName[3] == dissolvedAddress[3]); +#ifndef _WIN32 + Require(localName[4] == 0 && localName[5] == 0 && localName[6] == 0 && localName[7] == 0); +#endif + Require(send_nid_postfix(dissolved, "x", 1, 0) == -1 && *__error_nid_postfix() == 39); + const char unrelated[] = "unrelated sender"; + Require(sendto_nid_postfix(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); + queued = 0; + for (int i = 0; i < 1000 && queued == 0; ++i) { + Require(ioctl_nid_postfix(dissolved, 0x4004667f, &queued) == 0); + if (!queued) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(queued == sizeof(unrelated)); + Require(recvfrom_nid_postfix(dissolved, buffer, sizeof(buffer), 0, nullptr, nullptr) == sizeof(unrelated)); + Require(std::strcmp(buffer, unrelated) == 0); + for (const int descriptor : {peer, other, dissolved}) Require(close_nid_postfix(descriptor) == 0); unsigned char parsed[16]{}; Require(inet_pton_nid_postfix(2, "127.0.0.1", parsed) == 1); Require(parsed[0] == 127 && parsed[1] == 0 && parsed[2] == 0 && parsed[3] == 1); diff --git a/core/libs/tests/GuestUltQueue.cpp b/core/libs/tests/GuestUltQueue.cpp index 1ab7d5e978..2a93f78c0c 100644 --- a/core/libs/tests/GuestUltQueue.cpp +++ b/core/libs/tests/GuestUltQueue.cpp @@ -98,14 +98,6 @@ static void Validation() { try { sceUltQueueDataResourcePoolGetWorkAreaSize(1, UINT64_MAX, 1); } catch (const std::out_of_range&) { overflow = true; } CHECK(overflow); - bool rejected = false; - try { sceUltQueueCreate(&extra, nullptr, 8, nullptr, &fixture.pool, &extra, 0); } - catch (const std::runtime_error&) { rejected = true; } - CHECK(rejected); - rejected = false; - try { sceUltQueueDataResourcePoolCreate(&extra, nullptr, 1, 8, 1, nullptr, nullptr, &extra, 0); } - catch (const std::runtime_error&) { rejected = true; } - CHECK(rejected); overflow = false; try { sceUltQueueDataResourcePoolGetWorkAreaSize(UINT32_MAX, UINT64_MAX / 2, 1); } catch (const std::out_of_range&) { overflow = true; } @@ -116,6 +108,14 @@ static void Validation() { CHECK(sceUltQueueDataResourcePoolDestroy(&fixture.pool) == State); fixture.Create(1); fixture.Destroy(); + std::array optParam{}; + optParam.fill(0x5A); + CHECK(sceUltQueueDataResourcePoolCreate(&fixture.pool, "pool", 1, 8, 1, nullptr, nullptr, optParam.data(), 0) == 0); + CHECK(sceUltQueueCreate(&fixture.first, "first", 8, nullptr, &fixture.pool, optParam.data(), 0) == 0); + std::uint64_t output = 0; + CHECK(sceUltQueuePush(&fixture.first, &value) == 0); + CHECK(sceUltQueuePop(&fixture.first, &output) == 0 && output == value); + fixture.Destroy(); } static void Fifo() { diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index e91e1da8c6..59e59542b0 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -3,7 +3,11 @@ #include #include #include +#include #include +#include +#include +#include #include namespace { @@ -69,14 +73,168 @@ bool Throws(TCall call) { return false; } +struct HostTransfer { + void* deviceHandle; + std::uint8_t flags; + std::uint8_t endpoint; + std::uint8_t type; + std::uint32_t timeout; + std::int32_t status; + std::int32_t length; + std::int32_t actualLength; + void (APS5_VABI* callback)(HostTransfer*); + void* userData; + std::uint8_t* buffer; + std::int32_t numIsoPackets; +}; + +void APS5_VABI MarkDone(HostTransfer* transfer) { + *static_cast(transfer->userData) += 1; } -int main() { +void APS5_VABI MarkDoneThenSignal(HostTransfer* transfer) { + auto* notified = static_cast*>(transfer->userData); + notified->store(true, std::memory_order_release); +} + +void Pump(const int& done) { + const UsbdTimeval slice{0, 100000}; + for (int round = 0; round < 50 && done == 0; ++round) Require(sceUsbdHandleEventsTimeout(&slice) == 0, "event handling failed while a transfer was in flight"); +} + +void Device() { + const char* wanted = std::getenv("APS5_USBD_DEVICE"); + Require(wanted != nullptr, "APS5_USBD_DEVICE is not set to vendor:product in hex"); + const unsigned long vendor = std::strtoul(wanted, nullptr, 16); + const char* separator = std::strchr(wanted, ':'); + Require(separator != nullptr, "APS5_USBD_DEVICE has no ':'"); + const unsigned long product = std::strtoul(separator + 1, nullptr, 16); + void** list = nullptr; + const std::int64_t count = sceUsbdGetDeviceList(&list); + void* device = nullptr; + std::uint8_t descriptor[18] = {}; + for (std::int64_t index = 0; index < count && device == nullptr; ++index) { + Require(sceUsbdGetDeviceDescriptor(list[index], descriptor) == 0, "device descriptor failed"); + if ((descriptor[8] | descriptor[9] << 8) == vendor && (descriptor[10] | descriptor[11] << 8) == product) device = sceUsbdRefDevice(list[index]); + } + sceUsbdFreeDeviceList(list, 1); + Require(device != nullptr, "the APS5_USBD_DEVICE device was not listed"); + std::printf("device bus %u address %u\n", sceUsbdGetBusNumber(device), sceUsbdGetDeviceAddress(device)); + void* config = nullptr; + Require(sceUsbdGetActiveConfigDescriptor(device, &config) == 0 && config != nullptr, "active config descriptor failed"); + const auto* raw = static_cast(config); + Require(raw[1] == 2 && raw[4] > 0, "active config descriptor is not a configuration with interfaces"); + const std::uint8_t interfaces = raw[4]; + sceUsbdFreeConfigDescriptor(config); + Require(Throws([&] { sceUsbdFreeConfigDescriptor(config); }), "a config descriptor was freed twice"); + Require(sceUsbdGetConfigDescriptor(device, 0, &config) == 0, "config descriptor 0 failed"); + sceUsbdFreeConfigDescriptor(config); + void* handle = nullptr; + Require(sceUsbdOpen(device, &handle) == 0 && handle != nullptr, "opening the device failed"); + Require(sceUsbdCheckConnected(handle) == 0, "an open device was reported disconnected"); + std::uint8_t text[256] = {}; + if (descriptor[15] != 0) { + const std::int32_t length = sceUsbdGetStringDescriptor(handle, descriptor[15], 0x409, text, sizeof(text)); + Require(length >= 2 && text[1] == 3, "product string descriptor failed"); + std::string product_name; + for (std::int32_t index = 2; index + 1 < length; index += 2) product_name += static_cast(text[index]); + std::printf("product \"%s\"\n", product_name.c_str()); + } + std::uint8_t again[18] = {}; + Require(sceUsbdControlTransfer(handle, 0x80, 6, 0x0100, 0, again, sizeof(again), 1000) == 18, "synchronous device descriptor request failed"); + Require(std::memcmp(again, descriptor, sizeof(again)) == 0, "device descriptor read over the wire differs from the cached one"); + for (std::int32_t interfaceNumber = 0; interfaceNumber < interfaces; ++interfaceNumber) { + Require(sceUsbdClaimInterface(handle, interfaceNumber) == 0, "claiming an interface failed"); + Require(sceUsbdReleaseInterface(handle, interfaceNumber) == 0, "releasing an interface failed"); + } + Require(sceUsbdReleaseInterface(handle, 0) == static_cast(0x80240005), "releasing an unclaimed interface did not report not found"); + Require(sceUsbdGetConfigDescriptor(device, 0, &config) == 0, "config descriptor 0 for the endpoint scan failed"); + const std::uint8_t* config0 = static_cast(config); + const std::uint8_t* interfaceArray = *reinterpret_cast(config0 + 16); + std::uint8_t interruptAddress = 0; + std::uint16_t interruptMax = 0; + if (interfaceArray == nullptr) { + Require(false, "the active config descriptor has no interface array"); + } + for (std::uint8_t index = 0; index < interfaces && interruptAddress == 0; ++index) { + const std::uint8_t* alternate = *reinterpret_cast(interfaceArray + index * 16); + if (alternate == nullptr) continue; + const std::uint8_t* endpoints = *reinterpret_cast(alternate + 16); + if (endpoints == nullptr) continue; + for (std::uint8_t endpoint = 0; endpoint < alternate[4]; ++endpoint) { + const std::uint8_t* item = endpoints + endpoint * 32; + if ((item[2] & 0x80) != 0 && (item[3] & 3) == 3) { + interruptAddress = item[2]; + interruptMax = static_cast(item[4] | item[5] << 8); + } + } + } + Require(interruptAddress != 0 && interruptMax != 0, "the device has no interrupt IN endpoint"); + sceUsbdFreeConfigDescriptor(config); + + auto* transfer = static_cast(sceUsbdAllocTransfer(0)); + std::uint8_t setup[8 + 18] = {0x80, 6, 0x00, 0x01, 0x00, 0x00, 18, 0}; + int done = 0; + transfer->deviceHandle = handle; + transfer->type = 0; + transfer->timeout = 1000; + transfer->buffer = setup; + transfer->length = sizeof(setup); + transfer->callback = MarkDone; + transfer->userData = &done; + Require(sceUsbdSubmitTransfer(transfer) == 0, "submitting an asynchronous control transfer failed"); + Require(Throws([&] { sceUsbdFreeTransfer(transfer); }), "an in-flight transfer was freed"); + Pump(done); + Require(done == 1 && transfer->status == 0 && transfer->actualLength == 18, "asynchronous control transfer did not complete"); + Require(std::memcmp(setup + 8, descriptor, 18) == 0, "asynchronous device descriptor differs from the cached one"); + bool broken = false; + { + const UsbdTimeval slice{0, 200}; + for (int round = 0; round < 300 && !broken; ++round) { + std::atomic notified{false}; + std::uint8_t blink[64] = {}; + sceUsbdFillInterruptTransfer(transfer, handle, interruptAddress, blink, interruptMax, reinterpret_cast(&MarkDoneThenSignal), ¬ified, 0); + const int submitResult = sceUsbdSubmitTransfer(transfer); + if (submitResult != 0) { + broken = true; + break; + } + std::thread canceller([&] { + const int outcome = sceUsbdCancelTransfer(transfer); + if (outcome != 0 && outcome != static_cast(0x80240005)) broken = true; + }); + while (!notified.load(std::memory_order_acquire)) { + if (sceUsbdHandleEventsTimeout(&slice) != 0) broken = true; + } + canceller.join(); + if (transfer->status != 0 && transfer->status != 2 && transfer->status != 3) broken = true; + } + } + Require(!broken, "the cancel race failed"); + + Require(sceUsbdCancelTransfer(transfer) == static_cast(0x80240005), "cancelling a completed transfer did not report not found"); + sceUsbdFreeTransfer(transfer); + sceUsbdClose(handle); + Require(Throws([&] { sceUsbdClose(handle); }), "a handle was closed twice"); + sceUsbdUnrefDevice(device); + std::puts("USBD device tests passed"); +} + +} + +int main(int argc, char** argv) { Require(sceUsbdInit() == 0, "initialization failed"); void** list = nullptr; - Require(sceUsbdGetDeviceList(&list) == 0, "a device was listed"); - Require(list != nullptr && list[0] == nullptr, "device list is not an empty null-terminated array"); + const std::int64_t count = sceUsbdGetDeviceList(&list); + Require(count >= 0 && list != nullptr && list[count] == nullptr, "device list is not a null-terminated array of the reported length"); + for (std::int64_t index = 0; index < count; ++index) { + std::uint8_t deviceDescriptor[18] = {}; + Require(sceUsbdGetDeviceDescriptor(list[index], deviceDescriptor) == 0 && deviceDescriptor[0] == 18 && deviceDescriptor[1] == 1, "listed device has no device descriptor"); + Require(sceUsbdRefDevice(list[index]) == list[index], "listed device was not referenced"); + sceUsbdUnrefDevice(list[index]); + } sceUsbdFreeDeviceList(list, 1); + Require(Throws([&] { sceUsbdFreeDeviceList(list, 1); }), "a device list was freed twice"); Require(sceUsbdGetDeviceList(nullptr) == invalidArgument, "null list accepted"); Require(sceUsbdHandleEventsTimeout(nullptr) == invalidArgument, "null timeout accepted"); const UsbdTimeval invalid{0, 1000000}; @@ -135,6 +293,7 @@ int main() { sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); sceUsbdFreeConfigDescriptor(nullptr); + if (argc == 2 && std::strcmp(argv[1], "device") == 0) Device(); sceUsbdExit(); std::puts("USBD tests passed"); return 0; diff --git a/core/libs/tests/GuestVideoOutHighFrameRate.cpp b/core/libs/tests/GuestVideoOutHighFrameRate.cpp new file mode 100644 index 0000000000..3e4f4c4437 --- /dev/null +++ b/core/libs/tests/GuestVideoOutHighFrameRate.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutOpen(int, int, int, const void*); +int APS5_VABI sceVideoOutClose(int); +int APS5_VABI sceVideoOutIsOutputSupported(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutConfigureOutput(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutWaitVblank(int); +int APS5_VABI sceVideoOutGetOutputStatus(int, VideoOutOutputStatus*); +int APS5_VABI sceVideoOutGetResolutionStatus(int, VideoOutResolutionStatus*); +} + +static constexpr int Unsupported = static_cast(0x80290016u); + +static void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +static double Measure(int handle) { + Require(sceVideoOutWaitVblank(handle) == 0, "initial vblank failed"); + const auto start = std::chrono::steady_clock::now(); + for (int frame = 0; frame < 60; ++frame) Require(sceVideoOutWaitVblank(handle) == 0, "vblank failed"); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +int main(int argc, char** argv) { + const bool requireDisplay = argc > 1 && std::strcmp(argv[1], "--require-high-frame-rate") == 0; + VideoOutTestEnvironment environment; + { + std::ofstream param("app0/sce_sys/param.json", std::ios::binary); + param << R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"High frame rate test"}},"attribute3":64})"; + Require(static_cast(param), "cannot write high-frame-rate fixture"); + } + int handle; + try { + handle = sceVideoOutOpen(255, 0, 0, nullptr); + } catch (const std::runtime_error& error) { + if (requireDisplay) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + const int supported = sceVideoOutIsOutputSupported(handle, 15, nullptr, nullptr, 0); + if (supported == Unsupported && !requireDisplay) { + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == Unsupported, "unsupported display accepted high-frame-rate output"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); + std::puts("skipped, current display does not provide 120 Hz"); + return 77; + } + Require(supported == 1, "120 Hz output not reported as supported"); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode failed"); + const double normal = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == 0, "permitted high-frame-rate mode failed"); + VideoOutOutputStatus output{}; + VideoOutResolutionStatus resolution{}; + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 13, "wrong output refresh rate"); + Require(sceVideoOutGetResolutionStatus(handle, &resolution) == 0 && resolution.refreshRate == 13, "wrong resolution refresh rate"); + const double fast = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode restore failed"); + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 3, "default output refresh rate not restored"); + const double restored = Measure(handle); + std::printf("60 vblanks: default=%.3f s high=%.3f s restored=%.3f s\n", normal, fast, restored); + Require(normal > 0.8 && normal < 1.3 && fast > 0.4 && fast < 0.7 && restored > 0.8 && restored < 1.3, "incorrect vblank periods"); + Require(normal / fast > 1.7 && restored / fast > 1.7, "high-frame-rate mode did not double vblank frequency"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); +} diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 01b657eacb..da708754d1 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -52,7 +52,10 @@ int main() { Require(handle > 0); Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); - for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0xFull, 0x10ull, 0x11ull, 0x13ull}) { + const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); + Require(highFrameRate == 1 || highFrameRate == UNSUPPORTED_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, 15, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); + for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0x10ull, 0x11ull, 0x13ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); Require(sceVideoOutConfigureOutput(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 9e9f02fe00..9c5ef4b12b 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,8 +6,10 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +int APS5_VABI sceWebBrowserDialogGetResult(void* result); } namespace { @@ -15,20 +17,47 @@ namespace { constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); void Require(bool value) { if (!value) std::abort(); } } int main() { + std::uint8_t param[64] = {}; + std::int32_t result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(result == 0x5a5a5a5a); + Require(sceWebBrowserDialogInitialize() == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); + Require(result == 0x5a5a5a5a); + Require(sceWebBrowserDialogTerminate() == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); - std::uint8_t param[64] = {}; + Require(sceWebBrowserDialogOpen(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); Require(sceWebBrowserDialogOpen(param) == 0); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + Require(sceWebBrowserDialogGetResult(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + + return 0; } diff --git a/core/libs/tests/GuestWideConversion.cpp b/core/libs/tests/GuestWideConversion.cpp index 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 b5bb38d0fe..adba600ad6 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -1,14 +1,19 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestTime.hpp" #include #include #include #include +#include #include 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 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*); +extern "C" int* APS5_VABI __error_nid_postfix(); static int APS5_VABI Format(char16_t* buffer, std::size_t size, const char16_t* format, ...) { #ifdef _WIN32 @@ -167,12 +172,79 @@ static void CheckSwprintf() { Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); } +static bool WcsftimeThrows(char16_t* buffer, const char16_t* format, const GuestTm* time) { + try { + wcsftime_nid_postfix(buffer, 16, format, time); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void CheckWcsftime() { + GuestTm time{}; + time.tm_sec = 9; + time.tm_min = 5; + time.tm_hour = 13; + time.tm_mday = 29; + time.tm_mon = 1; + time.tm_year = 124; + time.tm_wday = 4; + time.tm_yday = 59; + time.tm_zone = "UTC"; + struct Case { + const char16_t* wide; + const char* narrow; + }; + const Case cases[] = { + {u"%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S"}, + {u"%a %b %j %p %Z", "%a %b %j %p %Z"}, + {u"%%", "%%"}, + {u"%k|%l|%v|%+", "%k|%l|%v|%+"}, + {u"\u00e9t\u00e9 %Y", "\xe9t\xe9 %Y"}, + }; + for (const auto& item : cases) { + char narrow[128]; + char16_t wide[128]; + const std::size_t length = strftime_nid_postfix(narrow, sizeof(narrow), item.narrow, &time); + Require(length != 0, "wcsftime reference strftime"); + Require(wcsftime_nid_postfix(wide, 128, item.wide, &time) == length, "wcsftime length matches strftime"); + for (std::size_t index = 0; index <= length; ++index) + Require(wide[index] == static_cast(narrow[index]), "wcsftime output matches strftime"); + } + char16_t buffer[32]; + Require(wcsftime_nid_postfix(buffer, 32, u"%Y-%m-%d %H:%M:%S", &time) == 19 + && buffer == std::u16string(u"2024-02-29 13:05:09"), "wcsftime date and time"); + Require(wcsftime_nid_postfix(buffer, 32, u"%a %b %j %p", &time) == 14 + && buffer == std::u16string(u"Thu Feb 060 PM"), "wcsftime names"); + Require(wcsftime_nid_postfix(buffer, 32, u"%%", &time) == 1 && buffer == std::u16string(u"%"), "wcsftime percent"); + Require(wcsftime_nid_postfix(buffer, 20, u"%Y-%m-%d %H:%M:%S", &time) == 19, "wcsftime exact fit"); + + int& error = *__error_nid_postfix(); + for (auto& unit : buffer) unit = u'x'; + error = 5; + Require(wcsftime_nid_postfix(buffer, 19, u"%Y-%m-%d %H:%M:%S", &time) == 0 && error == 5 && buffer[0] == u'x', + "wcsftime overflow"); + Require(wcsftime_nid_postfix(buffer, 1, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime one unit"); + Require(wcsftime_nid_postfix(buffer, 0, u"%Y", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime zero size"); + Require(wcsftime_nid_postfix(buffer, 32, u"", &time) == 0 && error == 5 && buffer[0] == u'x', "wcsftime empty format"); + Require(wcsftime_nid_postfix(nullptr, 32, u"%Y", &time) == 0 && error == 5, "wcsftime null buffer"); + Require(wcsftime_nid_postfix(buffer, 32, u"%Y \u20ac", &time) == 0 && error == 86 && buffer[0] == u'x', + "wcsftime unconvertible format"); + error = 5; + Require(wcsftime_nid_postfix(buffer, SIZE_MAX, u"%Y", &time) == 0 && error == 22 && buffer[0] == u'x', + "wcsftime oversized buffer"); + Require(WcsftimeThrows(buffer, nullptr, &time), "wcsftime null format"); + Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); +} + int main() { CheckBounded(); CheckPositional(); CheckPointer(); CheckCount(); CheckSwprintf(); + CheckWcsftime(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 64dd0afc3a..101ea50e68 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -1,8 +1,13 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include +#include +#include +#include extern "C" { const char16_t* APS5_VABI wmemchr_nid_postfix(const char16_t* s, char16_t c, std::size_t n); @@ -13,6 +18,7 @@ std::size_t APS5_VABI wcslen_nid_postfix(const char16_t* s); int APS5_VABI wcscmp_nid_postfix(const char16_t* s1, const char16_t* s2); int APS5_VABI wcsncmp_nid_postfix(const char16_t* s1, const char16_t* s2, std::size_t n); char16_t* APS5_VABI wcscpy_nid_postfix(char16_t* dest, const char16_t* src); +char16_t* APS5_VABI wcscat_nid_postfix(char16_t* dest, const char16_t* src); char16_t* APS5_VABI wcsncpy_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); const char16_t* APS5_VABI wcschr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); @@ -69,6 +75,14 @@ int main() { require(wcscpy_nid_postfix(buffer, u"wide") == buffer); require(same(buffer, u"wide", 5) && buffer[5] == 0xaaaa); + char16_t concatenated[8]; + wmemset_nid_postfix(concatenated, 0xaaaa, 8); + concatenated[0] = 0; + require(wcscat_nid_postfix(concatenated, u"ab") == concatenated); + require(wcscat_nid_postfix(concatenated, u"世") == concatenated); + const char16_t expectedConcatenated[] = {u'a', u'b', u'世', 0, 0xaaaa}; + require(same(concatenated, expectedConcatenated, 5)); + wmemset_nid_postfix(buffer, 0xaaaa, 8); require(wcsncpy_nid_postfix(buffer, u"ab", 5) == buffer); const char16_t padded[] = {u'a', u'b', 0, 0, 0, 0xaaaa}; @@ -173,6 +187,13 @@ int main() { require(wcstof_nid_postfix(u"0.25", nullptr) == 0.25f); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); + const char16_t* wideNan = u" -nan(0x123)é"; + require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); + const long double wideExtended = wcstold_nid_postfix(wideNan, &end); + std::uint64_t significand; + std::memcpy(&significand, &wideExtended, sizeof(significand)); + require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12); require(wcscoll_nid_postfix(u"a", u"b") < 0); require(wcscoll_nid_postfix(u"￿", u"a") > 0); @@ -181,7 +202,33 @@ int main() { char16_t transformed[8]; wmemset_nid_postfix(transformed, 0xaaaa, 8); require(wcsxfrm_nid_postfix(transformed, u"wide", 8) == 4 && same(transformed, u"wide", 5) && transformed[5] == 0xaaaa); + wmemset_nid_postfix(transformed, 0xaaaa, 8); require(wcsxfrm_nid_postfix(transformed, u"much too long", 4) == 13); + if (!same(transformed, u"muc", 4)) std::fputs("wcsxfrm did not copy and terminate its truncated result\n", stderr); + require(same(transformed, u"muc", 4) && transformed[4] == 0xaaaa); require(wcsxfrm_nid_postfix(nullptr, u"abc", 0) == 3); + struct TransformCase { + const char16_t* source; + std::size_t count; + std::size_t length; + const char16_t* expected; + std::size_t written; + }; + for (const TransformCase test : { + TransformCase{u"wide", 0, 4, u"", 0}, + TransformCase{u"wide", 1, 4, u"", 1}, + TransformCase{u"wide", 4, 4, u"wid", 4}, + TransformCase{u"wide", 5, 4, u"wide", 5}, + TransformCase{u"", 0, 0, u"", 0}, + TransformCase{u"", 1, 0, u"", 1}, + TransformCase{u"", 8, 0, u"", 1}, + TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 4, 5, u"\x7fff\x8000\xffff", 4}, + TransformCase{u"\x7fff\x8000\xffff\xd83d\xde00", 6, 5, u"\x7fff\x8000\xffff\xd83d\xde00", 6} + }) { + wmemset_nid_postfix(transformed, 0xaaaa, 8); + require(wcsxfrm_nid_postfix(transformed, test.source, test.count) == test.length); + require(same(transformed, test.expected, test.written)); + for (std::size_t index = test.written; index < 8; ++index) require(transformed[index] == 0xaaaa); + } return 0; } 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 dc3d03d871..de030efb03 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -59,8 +59,56 @@ static void CheckAlignment() { for (void* block : {zeroed, small, moved, posix}) sceLibcMspaceFree_nid_postfix(arena, block); Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); } +static void CheckAdjacentBlocks() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("adjacent", storage.data(), storage.size(), 0); + Require(arena != nullptr); + const auto end = [](void* block) { return static_cast(block) + sceLibcMspaceMallocUsableSize_nid_postfix(block); }; + std::array blocks{}; + for (std::size_t index = 0; index < blocks.size(); ++index) { + blocks[index] = sceLibcMspaceMalloc_nid_postfix(arena, index % 48 + 1); + Require(blocks[index] != nullptr); + if (index > 0) Require(blocks[index] == end(blocks[index - 1])); + } + void* last = blocks.back(); + Require(sceLibcMspaceRealloc_nid_postfix(arena, last, 80) == last); + void* after = sceLibcMspaceMalloc_nid_postfix(arena, 16); + Require(after == end(last)); + MallocManagedSize stats{sizeof(MallocManagedSize), 1, 0, 0, 0, 0, 0}; + Require(sceLibcMspaceMallocStats_nid_postfix(arena, &stats) == 0); + Require(stats.currentInuseSize == static_cast(end(after) - static_cast(blocks[0]))); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} +static void CheckMovedBlockKeepsUsableBytes() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("moved", storage.data(), storage.size(), 0); + Require(arena != nullptr); + const auto fill = [](void* block) { + const auto usable = sceLibcMspaceMallocUsableSize_nid_postfix(block); + Require(usable > 1); + for (std::size_t index = 0; index < usable; ++index) static_cast(block)[index] = static_cast(index + 1); + return usable; + }; + const auto kept = [](const void* block, std::size_t usable) { + for (std::size_t index = 0; index < usable; ++index) if (static_cast(block)[index] != static_cast(index + 1)) return false; + return true; + }; + void* block = sceLibcMspaceMalloc_nid_postfix(arena, 1); + const auto usable = fill(block); + Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); + void* moved = sceLibcMspaceRealloc_nid_postfix(arena, block, 4096); + Require(moved != nullptr && moved != block && kept(moved, usable)); + void* other = sceLibcMspaceMalloc_nid_postfix(arena, 1); + const auto otherUsable = fill(other); + Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); + void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 4096, 256); + Require(realigned != nullptr && realigned != other && kept(realigned, otherUsable)); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} int main() { CheckAlignment(); + CheckAdjacentBlocks(); + CheckMovedBlockKeepsUsableBytes(); alignas(16) std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("test", storage.data(), storage.size(), 0); Require(arena != nullptr); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index cfe365afba..dd24b91013 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" #include +#include #include #include #include @@ -33,6 +34,37 @@ static ParsedParamJson ParseText(const std::filesystem::path& path, const std::s return parseParamJson(path); } +static void CheckNumericLocale(const std::filesystem::path& path) { + struct RestoreLocale { + std::string previous = std::setlocale(LC_NUMERIC, nullptr); + ~RestoreLocale() { std::setlocale(LC_NUMERIC, previous.c_str()); } + } restore; + bool commaLocale = false; + for (const auto* name : {"French", "French_France.1252", "fr_FR.UTF-8", "de_DE.UTF-8"}) { + if (std::setlocale(LC_NUMERIC, name) != nullptr && std::string(std::localeconv()->decimal_point) == ",") { + commaLocale = true; + break; + } + } + if (!commaLocale) { +#ifdef _WIN32 + throw std::runtime_error("No Windows comma-decimal locale available for regression"); +#else + std::cout << "Comma-decimal locale regression unavailable on this host\n"; + return; +#endif + } + for (const auto& [number, expected] : {std::pair{"1.25e3", std::uint64_t{1250}}, + std::pair{"0.5e3", std::uint64_t{500}}, std::pair{"12.75e2", std::uint64_t{1275}}, + std::pair{"1.25", std::uint64_t{1}}}) { + Require(ParseSize(path, number).downloadDataSizeMiB == expected, + std::string("JSON number depends on host locale: ") + number); + } + bool rejected = false; + try { ParseSize(path, "-0.5"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Negative fractional size was accepted under comma locale"); +} + static void CheckUserDefinedParams(const std::filesystem::path& path) { const auto parsed = ParseText(path, R"({"titleId":"PPSA23566","localizedParameters":{"en-US":{"titleName":"Example"}},)" R"("userDefinedParam1":23566,"userDefinedParam3":-7,"userDefinedParam4":2147483647})"); @@ -88,6 +120,19 @@ static void CheckLanguages(const std::filesystem::path& path) { Require(rejected, "Accepted localizedParameters without a language entry"); } +static void CheckOutputPermission(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"Example"}})"; + Require(ParseText(path, prefix + "}").attribute3 == 0, "Absent output permission is not zero"); + for (const auto& [text, expected] : {std::pair{"0", 0u}, {"524352", 0x80040u}, {"4294967295", 0xFFFFFFFFu}}) { + Require(ParseText(path, prefix + ",\"attribute3\":" + text + "}").attribute3 == expected, "Incorrect output permission"); + } + for (const auto* text : {"-1", "0.5", "1e3", "4294967296", "true", "null", "\"524352\""}) { + bool rejected = false; + try { ParseText(path, prefix + ",\"attribute3\":" + text + "}"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted invalid attribute3"); + } +} + static void CheckNumberSyntax(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; const std::pair fields[] = { @@ -134,6 +179,7 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); int result = 0; try { + CheckNumericLocale(path); CheckUtf8(path); CheckNumberSyntax(path); const std::pair sizes[] = { @@ -156,6 +202,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckOutputPermission(path); CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp index 69ce4d7c26..d84f2714a5 100644 --- a/core/libs/tests/PixelAncillary.cpp +++ b/core/libs/tests/PixelAncillary.cpp @@ -3,6 +3,7 @@ #include "Optimization/DeadCodeEliminator.hpp" #include "Optimization/MaskedSelectEliminator.hpp" #include "Optimization/ShaderInfoCollector.hpp" +#include "Recompiler.hpp" #include #include #include @@ -18,8 +19,25 @@ struct Row { std::uint32_t sample; std::uint32_t layer; bool frontFacing; + RasterPrimitive primitive = RasterPrimitive::Triangle; + bool layerExported = false; }; -constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}}}; +constexpr std::array Rows {{{0u, 0u, false}, {0u, 0u, true}, {15u, 0x1FFFu, false}, {15u, 0x1FFFu, true}, {0u, 0x1FFFu, true}, {15u, 0u, false}, + {0u, 0x1FFFu, false, RasterPrimitive::Triangle, true}, {15u, 0x1FFFu, true, RasterPrimitive::Triangle, true}, {15u, 3u, false, RasterPrimitive::Triangle, true}, {0u, 0u, true, RasterPrimitive::Triangle, true}}}; + +static std::uint32_t Evaluate(const IrValue* value, const Row& row); +static std::uint32_t BitField(std::uint32_t word, std::uint32_t offset, std::uint32_t count, bool isSigned); +static void RequireField(const IrValue& user, StageInputKind kind, std::uint32_t fieldOffset, std::uint32_t count) { + if (kind == StageInputKind::Layer) { + for (const Row& row : Rows) Require(Evaluate(&user, row) == (row.layerExported ? BitField(row.layer, fieldOffset, count, user.Opcode() == IrOpcode::BitFieldSExtract) : 0u)); + return; + } + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} static IrBlock& Begin(IrProgram& program) { program.Resources().stage = IrShaderStage::Pixel; @@ -78,11 +96,7 @@ static void ExtractVector(std::uint32_t offset, std::uint32_t count, StageInputK static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void ExtractVectorMaskedCopy(std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { @@ -107,21 +121,13 @@ static void ExtractVectorMaskedCopy(std::uint32_t offset, std::uint32_t count, S static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {©})); static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); ConstantFolder().Fold(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { IrProgram program; auto& user = Build(program, opcode, offset, count); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); - Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); - Require(user.Argument(2)->Resolve()->ImmediateU32() == count); + RequireField(user, kind, fieldOffset, count); } static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect = false) { IrProgram program; @@ -153,11 +159,14 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { case StageInputKind::SampleId: return row.sample; case StageInputKind::Layer: return row.layer; case StageInputKind::FrontFacing: return row.frontFacing ? 1u : 0u; + case StageInputKind::RasterPrimitive: return static_cast(row.primitive); + case StageInputKind::LayerExported: return row.layerExported ? 1u : 0u; default: break; } break; case IrOpcode::ShiftLeftLogical32: return Evaluate(value->Argument(0), row) << Evaluate(value->Argument(1), row); case IrOpcode::BitwiseOr32: return Evaluate(value->Argument(0), row) | Evaluate(value->Argument(1), row); + case IrOpcode::IMul32: return Evaluate(value->Argument(0), row) * Evaluate(value->Argument(1), row); case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: return BitField(Evaluate(value->Argument(0), row), Evaluate(value->Argument(1), row), Evaluate(value->Argument(2), row), value->Opcode() == IrOpcode::BitFieldSExtract); @@ -168,7 +177,7 @@ static std::uint32_t Evaluate(const IrValue* value, const Row& row) { throw std::runtime_error("packed pixel ancillary evaluation reached an unsupported value"); } static std::uint32_t PackedWord(const Row& row) { - return UnmodelledBits | (row.sample << 8u) | (row.layer << 16u); + return UnmodelledBits | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u); } static void Values(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, bool throughSelect) { IrProgram program; @@ -190,11 +199,7 @@ static void LateFold() { IrValue& user = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&ancillary, &offset, &count}); End(builder, user); Lower(program); - const IrValue* field = user.Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin); - Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); - Require(user.Argument(1)->Resolve()->ImmediateU32() == 0u); - Require(user.Argument(2)->Resolve()->ImmediateU32() == 11u); + RequireField(user, StageInputKind::Layer, 0u, 11u); for (const Row& row : Rows) { Require(Evaluate(&user, row) == BitField(PackedWord(row), 16u, 11u, false)); } @@ -305,27 +310,28 @@ static HelperProgram HelperOnly(IrProgram& program, HelperRead read, std::uint32 static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); return HelperProgram {&written, &layer, &value}; } -static void LowerMasked(IrProgram& program) { +static void LowerMasked(IrProgram& program, bool compose = false) { ConstantFolder().Fold(program); DeadCodeEliminator().RemoveIdentities(program); DeadCodeEliminator().Eliminate(program); static_cast(MaskedSelectEliminator().Eliminate(program)); DeadCodeEliminator().Eliminate(program); + if (compose && ConstantFolder().ComposePackedAncillary(program)) DeadCodeEliminator().Eliminate(program); const ShaderPixelInputInfo pixel {}; ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); } static void HelperLanesOnly(std::uint32_t chain = 0u) { IrProgram program; const HelperProgram built = HelperOnly(program, HelperRead::Export, chain); - LowerMasked(program); + LowerMasked(program, true); + Require(!program.Info().rasterPrimitive); for (const IrBlock* block : program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { Require(inst->Opcode() != IrOpcode::GetBuiltin || static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) != StageInputKind::PackedAncillary); } } Require(built.written->Parent() == nullptr); - const IrValue* field = built.layer->Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); const IrValue* entryValue = built.exported->Argument(0)->Resolve(); Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); } @@ -411,8 +417,7 @@ static void HelperLoopOnly(LoopRead read) { } } Require(built.written->Parent() == nullptr); - const IrValue* field = built.layer->Argument(0)->Resolve(); - Require(field->Opcode() == IrOpcode::GetBuiltin && static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == StageInputKind::Layer); + RequireField(*built.layer, StageInputKind::Layer, 0u, 11u); const IrValue* entryValue = built.exported->Argument(0)->Resolve(); Require(entryValue->HasImmediate() && entryValue->ImmediateU32() == SelectOther); } @@ -427,6 +432,67 @@ static void HelperLoopRefused(LoopRead read) { } Require(false); } +static bool BuiltinRemains(const IrProgram& program, StageInputKind kind) { + for (const IrBlock* block : program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && static_cast(inst->Argument(0)->Resolve()->ImmediateU32()) == kind) return true; + } + } + return false; +} +static bool AncillaryRemains(const IrProgram& program) { + return BuiltinRemains(program, StageInputKind::PackedAncillary); +} +static void ExportedWholeWord() { + IrProgram program; + auto& entry = Begin(program); + auto& body = program.CreateBlock(); + auto& merge = program.CreateBlock(); + program.BlockOrder().push_back(&body); + program.BlockOrder().push_back(&merge); + entry.AddBranch(&body); + entry.AddBranch(&merge); + body.AddBranch(&merge); + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + IrValue& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::HelperInvocation)), &builder.Constant(0u)}); + IrValue& live = builder.IEqual(helper, builder.Constant(0u)); + IrValue& ancillary = AncillaryBuiltin(builder); + IrValue& tested = builder.INotEqual(builder.Select(FrontFacingBuiltin(builder), builder.Constant(7u), builder.Constant(0u)), builder.Constant(0u)); + builder.SetInsertionPoint(body); + IrValue& written = builder.Select(live, builder.Constant(SelectOther), ancillary); + IrValue& carried = program.CreateValue(IrOpcode::Phi, IrType::U32); + merge.AppendInstruction(&carried); + carried.AddPhiOperand(&entry, &ancillary); + carried.AddPhiOperand(&body, &written); + builder.SetInsertionPoint(merge); + const auto channel = [&](float value) -> IrValue& { + IrValue& half = builder.ConstantF32(value); + return builder.Select(tested, builder.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&half, &half}), carried); + }; + IrValue& data = builder.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&channel(0.25f), &channel(0.75f), &builder.Constant(0u), &builder.Constant(0u)}); + program.Metadata().exportInfo.push_back(ExportInfo {.kind = ExportTargetKind::Mrt, .index = 0u, .en = 0xFu, .done = true, .vm = true}); + static_cast(builder.Emit(IrOpcode::SetAttribute, IrType::Void, {&data, &live}, ExportFlags {0u, 0u})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(BuiltinRemains(program, StageInputKind::LayerExported)); + const IrValue* word = carried.Argument(0)->Resolve(); + constexpr std::array words {{{0u, 0u, false, RasterPrimitive::Triangle}, {0u, 0u, false, RasterPrimitive::Rectangle}, {0u, 0u, false, RasterPrimitive::Line}, {15u, 0x1FFFu, false, RasterPrimitive::Point}, {3u, 0x7FFu, false, RasterPrimitive::Triangle}, + {15u, 0x7FFu, false, RasterPrimitive::Point, true}, {3u, 5u, false, RasterPrimitive::Triangle, true}}}; + for (const Row& row : words) { + Require(Evaluate(word, row) == (static_cast(row.primitive) | (row.sample << 8u) | (row.layerExported ? row.layer << 16u : 0u))); + } +} +static void WholeWordReplacesRefusal(HelperRead read) { + IrProgram program; + const HelperProgram built = HelperOnly(program, read); + LowerMasked(program, true); + Require(!AncillaryRemains(program) && program.Info().rasterPrimitive); + Require(built.written->Parent() != nullptr); + const Row row {9u, 0x123u, false, RasterPrimitive::Rectangle}; + Require(Evaluate(built.written->Argument(2), row) == (3u | (9u << 8u))); +} enum class StartingQuad { Kept, BallotAfterBranch, BallotOfOtherLanes, ComputeStage }; static IrValue& StartingQuadMasked(IrProgram& program, StartingQuad shape) { @@ -544,6 +610,10 @@ int main() { HelperLoopOnly(LoopRead::ExportInside); HelperLoopOnly(LoopRead::UniformLoop); HelperLoopRefused(LoopRead::RevivedExportInside); + ExportedWholeWord(); + WholeWordReplacesRefusal(HelperRead::FullWaveExport); + WholeWordReplacesRefusal(HelperRead::DataTestExport); + WholeWordReplacesRefusal(HelperRead::Ballot); StartingQuadOnly(); StartingQuadRefused(StartingQuad::BallotAfterBranch); StartingQuadRefused(StartingQuad::BallotOfOtherLanes); diff --git a/core/libs/tests/RegisterContext.cpp b/core/libs/tests/RegisterContext.cpp index 28c22057c6..5c45536bd4 100644 --- a/core/libs/tests/RegisterContext.cpp +++ b/core/libs/tests/RegisterContext.cpp @@ -12,7 +12,7 @@ #endif static_assert(sizeof(_Unwind_Context::registers) == 17 * sizeof(std::uintptr_t)); -static_assert(offsetof(_Unwind_Context, cfa) == 17 * sizeof(std::uintptr_t)); +static_assert(offsetof(_Unwind_Context, cfa) == offsetof(_Unwind_Context, registers) + 17 * sizeof(std::uintptr_t)); extern "C" void CaptureProbe(std::uintptr_t*); extern "C" void RestoreProbe(std::uintptr_t*, std::uintptr_t*); diff --git a/core/libs/tests/ThreadLocalDestructors.cmake b/core/libs/tests/ThreadLocalDestructors.cmake index b87abde071..d58f04de55 100644 --- a/core/libs/tests/ThreadLocalDestructors.cmake +++ b/core/libs/tests/ThreadLocalDestructors.cmake @@ -2,7 +2,7 @@ file(GLOB modules "${MODULES}/*.prx") if(NOT modules) message(FATAL_ERROR "no modules in ${MODULES}") endif() -set(explicit "__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix") +set(explicit "__cxa_thread_atexit_nid_postfix|__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix") set(failures "") foreach(module IN LISTS modules) get_filename_component(name "${module}" NAME) diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp new file mode 100644 index 0000000000..08b6d4ea12 --- /dev/null +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -0,0 +1,208 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +using Collection = Mappings::Collection; +constexpr std::size_t Small = 4096; +constexpr std::size_t Bytes = 65536; +std::atomic faults{0}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +LONG CALLBACK HandleWrite(EXCEPTION_POINTERS* exception) { + const auto* record = exception->ExceptionRecord; + if (record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || record->NumberParameters < 2 || record->ExceptionInformation[0] != 1) return EXCEPTION_CONTINUE_SEARCH; + if (!Mappings::Get().HandleWrite(record->ExceptionInformation[1])) return EXCEPTION_CONTINUE_SEARCH; + ++faults; + return EXCEPTION_CONTINUE_EXECUTION; +} + +struct Region { + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Bytes, nullptr); + unsigned char* first = static_cast(Mappings::Get().Reserve(nullptr, Bytes)); + unsigned char* alias = static_cast(Mappings::Get().Reserve(nullptr, Bytes)); + + Region() { + Require(section && first && alias, "cannot allocate shared test memory"); + Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE); + Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE); + } + ~Region() { + Mappings::Get().Reset(first, Bytes); + Mappings::Get().Reset(alias, Bytes); + VirtualFree(first, 0, MEM_RELEASE); + VirtualFree(alias, 0, MEM_RELEASE); + CloseHandle(section); + } + void Write(std::size_t page, unsigned char value) { + static_cast(first)[page * Small] = value; + } + std::set Collect(Collection mode, std::size_t begin = 0, std::size_t count = 16, bool clear = true, bool useAlias = false, std::size_t capacity = 32) { + auto* base = useAlias ? alias : first; + const auto address = reinterpret_cast(base); + auto cursor = address + begin * Small; + const auto end = cursor + count * Small; + std::set result; + while (cursor < end) { + std::array pages{}; + auto available = capacity; + Require(Mappings::Get().Collect(cursor, end - cursor, pages.data(), &available, clear, mode), "shared collection failed"); + for (std::size_t i = 0; i < available; ++i) result.insert((reinterpret_cast(pages[i]) - address) / Small); + if (available < capacity) break; + cursor = reinterpret_cast(pages[available - 1]) + Small; + } + return result; + } + DWORD Protection(std::size_t page, bool useAlias = false) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery((useAlias ? alias : first) + page * Small, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.Protect; + } +}; + +std::set Pages(std::size_t begin, std::size_t end) { + std::set result; + for (auto page = begin; page < end; ++page) result.insert(page); + return result; +} + +void CheckArmed() { + Region region; + Require(region.Collect(Collection::Armed).empty(), "import collected fresh memory"); + const auto before = faults.load(); + region.Write(0, 1); + region.Write(4, 2); + Require(faults == before, "import armed fresh memory"); + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "first partial collect failed"); + Require(region.Protection(0) == PAGE_READONLY && region.Protection(3) == PAGE_READWRITE, "partial collect did not protect only its page"); + region.Write(0, 3); + region.Write(4, 4); + Require(region.Collect(Collection::Armed) == Pages(0, 1), "armed collect lost a write or consumed fresh pages"); + Require(region.Protection(4) == PAGE_READWRITE, "armed collect protected another fresh view"); + Require(region.Collect(Collection::All) == Pages(1, 16), "ordinary collect lost fresh pages"); + Require(region.Collect(Collection::All).empty(), "cleared memory reports writes"); +} + +void CheckFresh() { + Region region; + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "cannot collect initial page"); + region.Write(0, 7); + Require(region.Collect(Collection::Fresh) == Pages(1, 16), "fresh collect consumed an existing dirty page"); + Require(region.Collect(Collection::Fresh).empty(), "fresh collect repeated initial writes"); + Require(region.Collect(Collection::Armed) == Pages(0, 1), "fresh collect cleared an existing write"); + region.Write(9, 9); + Require(region.Collect(Collection::Armed) == Pages(9, 10), "write after activation was missed"); +} + +void CheckAliasesAndCapacity() { + Region region; + Require(region.Collect(Collection::All, 0, 1) == Pages(0, 1), "initial collect failed"); + Require(region.Protection(0, true) == PAGE_READONLY, "alias was not protected"); + Require(region.Collect(Collection::Armed, 0, 16, true, true).empty(), "alias protection was mistaken for collection history"); + Require(region.Collect(Collection::Fresh, 0, 16, true, true, 1) == Pages(0, 16), "small output buffers lost fresh pages"); + Require(region.Collect(Collection::Fresh, 0, 16, true, true).empty(), "small output buffers did not finish fresh collection"); + static_cast(region.alias)[0] = 8; + Require(region.Collect(Collection::Armed) == Pages(0, 1), "alias write was missed or fresh neighbors were consumed"); + Require(region.Collect(Collection::Fresh) == Pages(1, 16), "alias collection changed another view's history"); + Mappings::Get().Reset(region.first, Bytes); + Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE); + Require(region.Collect(Collection::Armed).empty(), "remapped address kept old collection history"); + Require(region.Collect(Collection::Fresh) == Pages(0, 16), "remapped memory was not fresh"); +} + +void CheckPinsAndHostWrites() { + Region region; + const auto address = reinterpret_cast(region.first); + Mappings::Get().Pin(address, 16384); + Require(region.Collect(Collection::Fresh) == Pages(0, 16), "pinned fresh collection failed"); + Require(region.Protection(0) == PAGE_READWRITE, "pinned page was protected"); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "pinned writes were trusted as clean"); + Mappings::Get().Unpin(address, 16384); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "unpin was not invalidated"); + Require(region.Collect(Collection::Armed).empty(), "unpin did not restore tracking"); + Require(Mappings::Get().BeginHostWrite(address, 16384), "host write refused"); + region.Write(0, 3); + region.Collect(Collection::Armed); + region.Write(0, 4); + Mappings::Get().EndHostWrite(address, 16384); + Require(region.Collect(Collection::Armed) == Pages(0, 4), "host write completion was missed"); + Require(region.Collect(Collection::Armed).empty(), "completed host write stayed dirty"); +} + +void CheckFailureAndHoles() { + Region region; + const auto address = reinterpret_cast(region.first); + DWORD previous; + Mappings::Get().SetProtection(address + 16384, 16384, PAGE_NOACCESS); + Require(VirtualProtect(region.first + 16384, 16384, PAGE_NOACCESS, &previous), "cannot protect test page"); + std::array pages{}; + auto count = pages.size(); + Require(!Mappings::Get().Collect(address, Bytes, pages.data(), &count, true, Collection::Fresh), "fresh inaccessible page accepted"); + Require(region.Protection(0) == PAGE_READWRITE, "failed collect changed an earlier page"); + Mappings::Get().SetProtection(address + 16384, 16384, PAGE_READWRITE); + Require(VirtualProtect(region.first + 16384, 16384, PAGE_READWRITE, &previous), "cannot restore test page"); + Mappings::Get().Reset(region.first + 16384, 16384); + Require(region.Collect(Collection::Fresh) == std::set({0, 1, 2, 3, 8, 9, 10, 11, 12, 13, 14, 15}), "fresh collection did not tolerate an unmapped block neighbor"); + Require(region.Collect(Collection::Fresh).empty(), "holes caused fresh pages to repeat"); +} + +void CheckPrivate() { + auto& mappings = Mappings::Get(); + auto* memory = static_cast(mappings.Reserve(nullptr, Bytes)); + Require(memory != nullptr, "private reservation failed"); + mappings.Commit(memory, Bytes, PAGE_READWRITE, Bytes, true); + const auto address = reinterpret_cast(memory); + std::array pages{}; + std::size_t count = pages.size(); + mappings.Collect(address, Bytes, pages.data(), &count, true); + static_cast(memory)[Small] = 3; + count = pages.size(); + Require(mappings.Collect(address, Bytes, pages.data(), &count, true, Collection::Fresh) && count == 0, "fresh collect touched private memory"); + count = pages.size(); + Require(mappings.Collect(address, Bytes, pages.data(), &count, true, Collection::Armed) && count == 1 && pages[0] == memory + Small, "private write watch changed"); + mappings.Reset(memory, Bytes); + VirtualFree(memory, 0, MEM_RELEASE); +} + +void CheckConcurrentWrites() { + Region region; + std::atomic start{false}; + std::thread writer([&] { + while (!start.load(std::memory_order_acquire)) std::this_thread::yield(); + for (std::size_t page = 0; page < 16; ++page) region.Write(page, static_cast(page + 1)); + }); + start.store(true, std::memory_order_release); + region.Collect(Collection::Fresh); + writer.join(); + for (std::size_t page = 0; page < 16; ++page) Require(region.first[page * Small] == page + 1, "activation lost a concurrent write"); + region.Collect(Collection::Armed); + Require(region.Collect(Collection::Armed).empty(), "concurrent writes did not settle"); +} +} + +int main() { + auto* handler = AddVectoredExceptionHandler(1, HandleWrite); + try { + Require(handler != nullptr, "cannot install test handler"); + CheckArmed(); + CheckFresh(); + CheckAliasesAndCapacity(); + CheckPinsAndHostWrites(); + CheckFailureAndHoles(); + CheckPrivate(); + CheckConcurrentWrites(); + std::cout << "Windows deferred write-watch tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + RemoveVectoredExceptionHandler(handler); +} diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 62eaa1cff4..e670c9980f 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -1,11 +1,8 @@ -add_executable(relinker - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp +option(ANYPS5_RELINKER_CLI "Build the native relinker command line" ON) + +add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestStartup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/guest/GuestImageReader.cpp @@ -33,8 +30,6 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp @@ -71,33 +66,58 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/ValidationPolicy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/SysVDynamicSectionBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/CallRegistryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/Conversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/RelinkerPipeline.cpp ) -target_include_directories(relinker PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/cli/include +target_include_directories(relinker_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include - ${CMAKE_CURRENT_SOURCE_DIR}/../libs ) +target_compile_features(relinker_core PUBLIC cxx_std_20) +set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) + +if(BUILD_TESTING) + add_test_executable(relinker_conversion_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ConversionTests.cpp) + target_link_libraries(relinker_conversion_tests PRIVATE relinker_core) + add_test(NAME relinker_conversion COMMAND relinker_conversion_tests) + set_tests_properties(relinker_conversion PROPERTIES TIMEOUT 20) +endif() + +if(ANYPS5_BUILD_WASM) + target_compile_options(relinker_core PRIVATE -fexceptions) + add_subdirectory(wasm) +endif() + +if(NOT ANYPS5_RELINKER_CLI) + return() +endif() + +add_executable(relinker + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/NativeModules.cpp) +target_link_libraries(relinker PRIVATE relinker_core) +target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs) +set_target_properties(relinker PROPERTIES CXX_EXTENSIONS OFF) + if(BUILD_TESTING AND WIN32) - get_target_property(windowsAliasTestSources relinker SOURCES) - list(REMOVE_ITEM windowsAliasTestSources - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp) - add_test_executable(windows_guest_export_alias_tests - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp - ${windowsAliasTestSources}) - get_target_property(windowsAliasIncludes relinker INCLUDE_DIRECTORIES) - target_include_directories(windows_guest_export_alias_tests PRIVATE ${windowsAliasIncludes}) + add_test_executable(windows_guest_export_alias_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp) + target_link_libraries(windows_guest_export_alias_tests PRIVATE relinker_core) add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) endif() if(WIN32) + target_sources(relinker PRIVATE ${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_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) endif() @@ -364,6 +384,9 @@ if(BUILD_TESTING) if(Python3_Interpreter_FOUND) add_test(NAME cli_help COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/test_help.py $) + add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $) + 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 dynamic_segment COMMAND ${Python3_EXECUTABLE} @@ -418,6 +441,8 @@ if(BUILD_TESTING) add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) + add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} @@ -440,6 +465,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_dir.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) + add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $) add_test(NAME sce_module_path COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_path.py $) add_test(NAME linux_guest_stack COMMAND ${Python3_EXECUTABLE} @@ -468,10 +495,47 @@ if(BUILD_TESTING) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) + set(windows_stdio_host_arguments) + if(WIN32) + if(MINGW) + add_library(windows_stdio_host_ucrt SHARED elfpatcher/tests/WindowsStdioHost.cpp) + target_compile_options(windows_stdio_host_ucrt PRIVATE -mcrtdll=ucrt) + target_link_options(windows_stdio_host_ucrt PRIVATE -mcrtdll=ucrt -static-libgcc -static-libstdc++) + set_target_properties(windows_stdio_host_ucrt PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + + get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY) + set(mingwDlltoolExecutable "${testCompilerDirectory}/dlltool.exe") + if(NOT EXISTS "${mingwDlltoolExecutable}") + message(FATAL_ERROR "Cannot locate MinGW DLL tool: ${mingwDlltoolExecutable}") + endif() + set(msvcrtImportLibrary "${CMAKE_CURRENT_BINARY_DIR}/windows_stdio_msvcrt_import.a") + add_custom_command(OUTPUT "${msvcrtImportLibrary}" + COMMAND "${mingwDlltoolExecutable}" -d "${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsMsvcrt.def" + -l "${msvcrtImportLibrary}" -D msvcrt.dll + DEPENDS elfpatcher/tests/WindowsMsvcrt.def + VERBATIM) + add_custom_target(windows_stdio_msvcrt_import DEPENDS "${msvcrtImportLibrary}") + add_library(windows_stdio_host_msvcrt SHARED elfpatcher/tests/WindowsMsvcrtStdioHost.cpp) + add_dependencies(windows_stdio_host_msvcrt windows_stdio_msvcrt_import) + target_link_libraries(windows_stdio_host_msvcrt PRIVATE "${msvcrtImportLibrary}") + target_compile_options(windows_stdio_host_msvcrt PRIVATE -fno-builtin) + target_link_options(windows_stdio_host_msvcrt PRIVATE -nostdlib -Wl,--no-undefined -Wl,--entry,0) + set_target_properties(windows_stdio_host_msvcrt PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + else() + add_library(windows_stdio_host SHARED elfpatcher/tests/WindowsStdioHost.cpp) + set_target_properties(windows_stdio_host PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON) + list(APPEND windows_stdio_host_arguments $) + endif() + endif() add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py + $ ${windows_stdio_host_arguments}) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) + add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} @@ -482,6 +546,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $) add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $) + add_test(NAME long_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_long_paths.py $) + set_tests_properties(long_paths PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $) add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE} @@ -503,6 +570,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_init_array_roots.py $) add_test(NAME gnu_hash_analysis COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_gnu_hash_analysis.py $) + add_test(NAME guest_gnu_hash COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_gnu_hash.py $) + set_tests_properties(guest_gnu_hash PROPERTIES TIMEOUT 20) add_test(NAME to_rosetta COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_to_rosetta.py $) add_test(NAME syscall_number COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 7faa5353b4..0038cd80b0 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Cli { @@ -22,6 +23,8 @@ struct Args { bool autorun = false; bool windowsDiagnostics = false; bool windowsGui = false; + std::optional windowsFixedBase; + bool traceImports = false; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; @@ -29,6 +32,7 @@ struct Args { std::string runPath = "$ORIGIN/libs"; std::set excludedSceModules; std::vector moduleDirectories; + std::vector executables; }; Args ParseArgs(int argc, char* argv[]); diff --git a/core/relinker/cli/include/ConsoleProgress.hpp b/core/relinker/cli/include/ConsoleProgress.hpp new file mode 100644 index 0000000000..a05bf67795 --- /dev/null +++ b/core/relinker/cli/include/ConsoleProgress.hpp @@ -0,0 +1,112 @@ +#ifndef CLI_CONSOLEPROGRESS_HPP +#define CLI_CONSOLEPROGRESS_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace Cli { + +class ConsoleProgress final : public Domain::IProgress { +public: + explicit ConsoleProgress(std::ostream& stream) : stream(stream) {} + + void IncludeFile(std::string_view path) { + const auto separator = path.find_last_of("/\\"); + const auto name = path.substr(separator == std::string_view::npos ? 0 : separator + 1); + nameWidth = std::max(nameWidth, textWidth(name)); + } + + void Report(const Domain::ProgressEvent& event) override { + if (event.Completed > event.Total) throw std::logic_error("Invalid progress counters"); + const char* label = nullptr; + switch (event.Stage) { + case Domain::ProgressStage::Executable: label = "Executable"; break; + case Domain::ProgressStage::Modules: label = "PRX conversion"; break; + case Domain::ProgressStage::Syscalls: label = "Syscall scan"; break; + case Domain::ProgressStage::Decode: label = "Instruction scan"; break; + case Domain::ProgressStage::Lowering: label = "Instruction conversion"; break; + case Domain::ProgressStage::Imports: label = "Import analysis"; break; + case Domain::ProgressStage::Image: label = "Output image"; break; + case Domain::ProgressStage::Write: label = "Write output"; break; + default: throw std::logic_error("Unknown progress stage"); + } + const auto separator = event.Input.find_last_of("/\\"); + const auto name = event.Input.substr(separator == std::string::npos ? 0 : separator + 1); + if (event.Stage == Domain::ProgressStage::Executable) { + if (event.Completed == 0) Message("Converting " + name + "\n"); + else Finish(); + return; + } + if (event.Stage == Domain::ProgressStage::Imports) { + if (event.Completed == 0) { + Finish(); + stream << '\n'; + Message("Import analysis\n"); + } + else Finish(); + return; + } + const auto percent = event.Total == 0 ? 100u : static_cast(100.0L * event.Completed / event.Total); + const bool changed = lastStage != event.Stage || lastInput != event.Input; + const bool complete = event.Completed == event.Total; + const auto now = std::chrono::steady_clock::now(); + if (!changed && lastPercent == percent && event.Completed != 0) return; + if (!changed && !complete && now - lastUpdate < std::chrono::milliseconds(80)) return; + if (changed || messages) Finish(); + std::ostringstream line; + line << std::left << std::setw(22) << label; + line << " | " << name << std::string(nameWidth > textWidth(name) ? nameWidth - textWidth(name) : 0, ' '); + line << " | " << std::right << std::setw(3) << percent << '%'; + if (event.Stage == Domain::ProgressStage::Modules) line << " (" << event.Completed << '/' << event.Total << ')'; + const auto text = line.str(); + stream << '\r' << text; + if (lineWidth > text.size()) stream << std::string(lineWidth - text.size(), ' ') << '\r' << text; + stream << std::flush; + lineWidth = text.size(); + active = true; + lastStage = event.Stage; + lastInput = event.Input; + lastPercent = percent; + lastUpdate = now; + if (complete) Finish(); + } + + void Message(std::string_view message) override { + if (active) Finish(); + stream << message; + if (!message.empty() && message.back() != '\n') stream << '\n'; + stream << std::flush; + messages = true; + } + + void Finish() { + if (active || messages) stream << '\n' << std::flush; + active = false; + messages = false; + lineWidth = 0; + } + +private: + static std::size_t textWidth(std::string_view text) { + return static_cast(std::count_if(text.begin(), text.end(), [](unsigned char character) { return (character & 0xc0) != 0x80; })); + } + + std::size_t nameWidth = 0; + std::ostream& stream; + Domain::ProgressStage lastStage = Domain::ProgressStage::Write; + std::string lastInput; + unsigned lastPercent = 101; + std::size_t lineWidth = 0; + bool active = false; + bool messages = false; + std::chrono::steady_clock::time_point lastUpdate{}; +}; + +} + +#endif diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp new file mode 100644 index 0000000000..bd49e3773b --- /dev/null +++ b/core/relinker/cli/include/NativeModules.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace Cli { + +struct NativeModules { + std::filesystem::path Root; + std::vector RuntimeDirectories; + std::vector Modules; +}; + +NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); + +} diff --git a/core/relinker/cli/include/RuntimeLayout.hpp b/core/relinker/cli/include/RuntimeLayout.hpp new file mode 100644 index 0000000000..6ba34c3932 --- /dev/null +++ b/core/relinker/cli/include/RuntimeLayout.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +namespace Cli { + +class RuntimeLayout { +public: + static void Print(std::ostream& stream, const std::string& executable, const std::vector& modules, const std::vector& extraExecutables = {}) { + Node root; + for (const auto& module : modules) { + auto* node = &root; + std::size_t start = 0; + for (;;) { + const auto end = module.find('/', start); + const auto name = module.substr(start, end == std::string::npos ? end : end - start); + node = &node->children[name]; + if (end == std::string::npos) break; + start = end + 1; + } + } + stream << "Arrange the files like this:\n" + << "folder/\n" + << " |-- " << executable << "\n"; + for (const auto& extra : extraExecutables) stream << " |-- " << extra << '\n'; + stream << " |-- libs/\n" + << " |-- *.prx (system libraries)\n" + << " |-- app0/\n" + << " |-- \n"; + printChildren(stream, root, " "); + stream << '\n'; + } + +private: + struct Node { + std::map children; + }; + + static void printChildren(std::ostream& stream, const Node& node, const std::string& prefix) { + for (const auto& [name, child] : node.children) { + stream << prefix << "|-- " << name << (child.children.empty() ? "\n" : "/\n"); + printChildren(stream, child, prefix + " "); + } + } +}; + +} diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 21d055d6b9..fadcbcb364 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -1,14 +1,16 @@ #include #include #include +#include #include #include #include +#include namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--to-intel] [--to-rosetta] [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
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } @@ -21,6 +23,18 @@ Args ParseArgs(int argc, char* argv[]) { const std::string arg = argv[i]; if (arg == "--help") { args.showHelp = true; + } else if (arg == "--windows-fixed-base") { + if (args.windowsFixedBase) throw std::runtime_error("--windows-fixed-base cannot be repeated"); + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) + throw std::runtime_error("--windows-fixed-base requires an address"); + std::string_view value(argv[++i]); + 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 (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 == "--skip-syscall-check") { args.skipSyscallCheck = true; } else if (arg == "--trap-sdk-syscall-stubs") { @@ -40,6 +54,9 @@ Args ParseArgs(int argc, char* argv[]) { if (i + 1 >= argc) throw std::runtime_error("--module-dir requires a directory"); args.moduleDirectories.push_back(argv[++i]); + } else if (arg == "--executable") { + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--executable requires a nonempty path"); + args.executables.push_back(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; } else if (arg == "--to-rosetta") { @@ -69,6 +86,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsDiagnostics = true; } else if (arg == "--windows-gui") { args.windowsGui = true; + } else if (arg == "--trace-imports") { + args.traceImports = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); } else if (arg.empty()) { @@ -92,6 +111,9 @@ Args ParseArgs(int argc, char* argv[]) { if (args.skipSceModule && !args.moduleDirectories.empty()) throw std::runtime_error("--module-dir conflicts with --skip-sce-module"); + if (args.skipSceModule && !args.executables.empty()) + throw std::runtime_error("--executable conflicts with --skip-sce-module"); + if (args.toWindows && args.toMacos) throw std::runtime_error("--windows conflicts with --macos"); if (args.toRosetta && args.toWindows) @@ -102,6 +124,13 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsGui && !args.toWindows) throw std::runtime_error("--windows-gui requires --windows"); + if (args.windowsFixedBase && !args.toWindows) + throw std::runtime_error("--windows-fixed-base requires --windows"); + if (args.traceImports && !args.toWindows) + 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.showHelp && (args.inputPath.empty() || args.outputPath.empty())) throw std::runtime_error(Usage()); diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp new file mode 100644 index 0000000000..4943091c64 --- /dev/null +++ b/core/relinker/cli/src/NativeModules.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include + +namespace Cli { + +NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories) { + using namespace Relinker; + const auto root = std::filesystem::absolute(sceModulePath).lexically_normal(); + if (!std::filesystem::exists(root)) throw Domain::RelinkerException("Guest module parent directory does not exist: " + Io::Utf8Path(root)); + if (!std::filesystem::is_directory(root)) throw Domain::RelinkerException("Guest module parent path is not a directory: " + Io::Utf8Path(root)); + const auto missingModuleMessage = std::string(GuestModulePattern) + " was not found in: " + Io::Utf8Path(root) + ". Use --skip-sce-module to disable guest module processing."; + const auto singular = root / "sce_module"; + const auto plural = root / "sce_modules"; + const auto prx = root / "prx"; + const bool hasSingular = std::filesystem::exists(singular); + const bool hasPlural = std::filesystem::exists(plural); + const bool hasPrx = std::filesystem::exists(prx); + if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist in the guest module parent directory"); + if (!hasSingular && !hasPlural && !hasPrx) throw Domain::RelinkerException(missingModuleMessage); + std::vector directories; + if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); + if (hasPrx) directories.push_back(prx); + std::vector runtimeDirectories; + for (const auto& name : moduleDirectories) { + const auto relative = Io::NativePath(name).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") + throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name); + auto directory = (root / relative).lexically_normal(); + if (!directory.has_filename()) directory = directory.parent_path(); + if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); + if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); + directories.push_back(directory); + runtimeDirectories.push_back(directory); + } + std::vector paths; + std::set unmatchedExclusions = excludedModules; + const auto isElf = [](const std::filesystem::path& path, bool& self) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + if (stream.gcount() != 4) return false; + const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; + self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); + return byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + }; + for (const auto& directory : directories) { + if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (Io::Utf8Path(entry.path().filename()).ends_with(GuestModuleSuffix)) continue; + if (excludedModules.contains(Io::Utf8Path(entry.path().filename()))) { + unmatchedExclusions.erase(Io::Utf8Path(entry.path().filename())); + continue; + } + if (!entry.is_regular_file()) continue; + if (std::filesystem::equivalent(entry.path(), inputPath)) continue; + bool self = false; + if (isElf(entry.path(), self)) paths.push_back(entry.path()); + else if (self) + std::cout << "Skipping SELF container: " << entry.path().lexically_relative(root).generic_string() + << " (unwrap it before relinking)\n"; + } + } + if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); + std::sort(paths.begin(), paths.end()); + NativeModules result; + result.Root = root; + result.RuntimeDirectories = std::move(runtimeDirectories); + for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); + return result; +} + +} diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 3cc339e55d..4331181592 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -133,6 +133,15 @@ void excludeConflictsWithSkip() { expectThrows({"--exclude-sce-module", "m", "--skip-sce-module", "input.elf", "output.elf"}, message); } +void executables() { + const auto args = parse({"--executable", "picker/picker.bin", "input.elf", "--executable", "es-de/es-de.bin", "output.elf"}); + require(args.executables == std::vector{"picker/picker.bin", "es-de/es-de.bin"}, "--executable paths were not recorded in order"); + expectThrows({"--executable"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "--registry", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--skip-sce-module", "--executable", "picker.bin", "input.elf", "output.elf"}, "--executable conflicts with --skip-sce-module"); +} + void unusedFilter() { for (const auto& value : {std::string("0"), std::string("1"), std::string("2")}) { const auto args = parse({"unused-filter=" + value, "input.elf", "output.elf"}); @@ -197,6 +206,22 @@ void missingPositionals() { expectUsage({"--rpath", "/libs"}); } +void windowsFixedBase() { + require(!parse({"--windows", "input.elf", "output.exe"}).windowsFixedBase, "fixed base was enabled by default"); + for (const auto& value : {"0x400000", "0X400000", "4194304"}) { + const auto args = parse({"--windows-fixed-base", value, "--windows", "input.elf", "output.exe"}); + require(args.windowsFixedBase == 0x400000, "fixed base was parsed incorrectly"); + } + require(parse({"--windows", "--windows-fixed-base", "0x400000000", "input.elf", "output.exe"}).windowsFixedBase == 0x400000000, "fixed base was truncated to 32 bits"); + for (const auto& value : {"0", "0x10000", "0x400001", "0x", "-4194304", "+4194304", " 4194304", "0x400000junk", "18446744073709551616", "0x800000000000"}) + expectThrows({"--windows", "--windows-fixed-base", value, "input.elf", "output.exe"}, "--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000"); + expectThrows({"--windows-fixed-base"}, "--windows-fixed-base requires an address"); + expectThrows({"--windows-fixed-base", "--windows"}, "--windows-fixed-base requires an address"); + expectThrows({"--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows"); + expectThrows({"--macos", "--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows"); + expectThrows({"--windows", "--windows-fixed-base", "0x400000", "--windows-fixed-base", "0x800000", "input.elf", "output.exe"}, "--windows-fixed-base cannot be repeated"); +} + } int main() { @@ -213,6 +238,7 @@ int main() { excludeSceModuleConsumesNextTokenVerbatim(); excludeSceModuleWithoutValue(); excludeConflictsWithSkip(); + executables(); unusedFilter(); unusedFilterRejectsBadValues(); unusedFilterRejectsDuplicates(); @@ -223,6 +249,7 @@ int main() { windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); missingPositionals(); + windowsFixedBase(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index e41f6cd538..43e5a7110b 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -65,6 +65,7 @@ struct DecodedInstructionInfo { std::uint8_t RexPrefix; ControlFlowKind FlowKind; bool HasRipRelativeDisp; + bool AddressSize32; std::size_t RipRelativeDispOffset; bool HasModRm; std::uint8_t ModRmByte; diff --git a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp index e133c533f1..ef17189275 100644 --- a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp +++ b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace Codegen { @@ -12,13 +13,10 @@ class IAmd64OnlyConverter { public: virtual ~IAmd64OnlyConverter() = default; - [[nodiscard]] virtual ConvertResult Convert( - std::vector fileBytes, - const std::vector& segments - ) const = 0; + [[nodiscard]] virtual ConvertResult Convert(std::vector fileBytes, const std::vector& segments) const = 0; }; -std::unique_ptr MakeAmd64OnlyConverter(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); +std::unique_ptr MakeAmd64OnlyConverter(Amd64OnlyTarget target = Amd64OnlyTarget::Intel, std::function progress = {}); } diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index 1f639525f5..969ea443b8 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -12,14 +13,10 @@ class IInstructionScanner { public: virtual ~IInstructionScanner() = default; - [[nodiscard]] virtual std::vector ScanCodeSection( - const std::vector& codeSection, - Domain::FileByteOffset codeSectionOffset, - Domain::FileByteOffset codeSectionSize - ) const = 0; + [[nodiscard]] virtual std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const = 0; }; -std::unique_ptr MakeInstructionScanner(); +std::unique_ptr MakeInstructionScanner(std::function progress = {}); } diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 6990cf40fb..f92c5edc93 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -209,7 +209,7 @@ private: class Amd64OnlyConverter : public IAmd64OnlyConverter { public: - explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {} + Amd64OnlyConverter(const Amd64OnlyTarget target, std::function progress) : _matcher(MakeAmd64OnlyInstructionMatcher(target)), progress(std::move(progress)) {} [[nodiscard]] ConvertResult Convert( std::vector fileBytes, @@ -224,13 +224,15 @@ private: }; std::unique_ptr _matcher; - std::unique_ptr _scanner = MakeInstructionScanner(); + std::function progress; void _convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, const std::vector& segments, - ConvertResult& result + ConvertResult& result, + std::uint64_t completed, + std::uint64_t total ) const; [[nodiscard]] static std::set _collectBranchTargets( @@ -290,29 +292,37 @@ void Amd64OnlyConverter::_convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, const std::vector& segments, - ConvertResult& result + ConvertResult& result, + std::uint64_t completed, + std::uint64_t total ) const { + if (ph.Offset > fileBytes.size() || ph.FileSize > fileBytes.size() - ph.Offset) + throw CodegenException("Code segment exceeds the file", ph.Offset); const auto segOffset = static_cast(ph.Offset); const auto segSize = static_cast(ph.FileSize); - if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset) - throw CodegenException("Code segment exceeds the file", ph.Offset); std::vector seg( fileBytes.begin() + static_cast(segOffset), fileBytes.begin() + static_cast(segOffset + segSize) ); - const auto matches = _atFileOffset(ph.Offset, [&] { return _scanner->ScanCodeSection(seg, 0, seg.size()); }); + const auto scanner = MakeInstructionScanner([&](auto offset, auto) { if (progress) progress(completed + offset, total); }); + const auto matches = _atFileOffset(ph.Offset, [&] { return scanner->ScanCodeSection(seg, 0, seg.size()); }); + if (progress) progress(completed + segSize, total); std::vector pending; for (std::size_t index = 0; index < matches.size(); ++index) { + if (progress && index != 0 && index % 4096 == 0) progress(completed + segSize + matches[index].Offset, total); const auto& match = matches[index]; auto substitution = _atFileOffset(ph.Offset + match.Offset, [&] { return _matcher->Match(seg.data() + match.Offset, match.Length); }); if (substitution.has_value()) pending.push_back({index, match, std::move(*substitution)}); } - if (pending.empty()) + if (progress) progress(completed + std::uint64_t{2} * segSize, total); + if (pending.empty()) { + if (progress) progress(completed + std::uint64_t{3} * segSize, total); return; + } const bool needsBranchTargets = std::any_of(pending.begin(), pending.end(), [](const Pending& item) { return item.Substitution.Lowering == Amd64OnlyLowering::Trampoline; @@ -324,7 +334,11 @@ void Amd64OnlyConverter::_convertSegment( pendingIndices.insert(item.Index); std::set claimed; std::set consumed; + + std::size_t processed = 0; for (const auto& item : pending) { + if (progress && processed != 0 && processed % 256 == 0) progress(completed + std::uint64_t{2} * segSize + item.Instruction.Offset, total); + ++processed; if (consumed.contains(item.Index)) continue; const auto& match = item.Instruction; @@ -425,6 +439,7 @@ void Amd64OnlyConverter::_convertSegment( if (seg.size() != segSize) throw CodegenException("Code segment size changed during Intel conversion", ph.Offset); std::copy(seg.begin(), seg.end(), fileBytes.begin() + static_cast(segOffset)); + if (progress) progress(completed + std::uint64_t{3} * segSize, total); } ConvertResult Amd64OnlyConverter::Convert( @@ -432,18 +447,28 @@ ConvertResult Amd64OnlyConverter::Convert( const std::vector& segments ) const { ConvertResult result{}; + std::uint64_t total = 0; for (const auto& ph : segments) { - if (ph.Type == kLoadSegment && (ph.Flags & kExecutableSegment) != 0) - _convertSegment(fileBytes, ph, segments, result); + if (ph.Type != kLoadSegment || (ph.Flags & kExecutableSegment) == 0) continue; + if (ph.FileSize > (std::numeric_limits::max() - total) / 3) throw CodegenException("Instruction conversion progress size overflow"); + total += 3 * ph.FileSize; } + if (progress) progress(0, total); + std::uint64_t completed = 0; + for (const auto& ph : segments) { + if (ph.Type != kLoadSegment || (ph.Flags & kExecutableSegment) == 0) continue; + _convertSegment(fileBytes, ph, segments, result, completed, total); + completed += 3 * ph.FileSize; + } + if (progress) progress(total, total); result.Bytes = std::move(fileBytes); return result; } } -std::unique_ptr MakeAmd64OnlyConverter(const Amd64OnlyTarget target) { - return std::make_unique(target); +std::unique_ptr MakeAmd64OnlyConverter(const Amd64OnlyTarget target, std::function progress) { + return std::make_unique(target, std::move(progress)); } } diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index ccd03f9834..3cd662a590 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -3,30 +3,31 @@ #include #include #include +#include #include namespace Codegen { class InstructionScanner : public IInstructionScanner { public: - [[nodiscard]] std::vector ScanCodeSection( - const std::vector& codeSection, - Domain::FileByteOffset codeSectionOffset, - Domain::FileByteOffset codeSectionSize - ) const override; + explicit InstructionScanner(std::function progress) : progress(std::move(progress)) {} + [[nodiscard]] std::vector ScanCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize) const override; + +private: + std::function progress; }; -std::vector InstructionScanner::ScanCodeSection( - const std::vector& codeSection, - const Domain::FileByteOffset codeSectionOffset, - const Domain::FileByteOffset codeSectionSize -) const { - const std::size_t limit = std::min(codeSection.size(), static_cast(codeSectionSize)); +std::vector InstructionScanner::ScanCodeSection(const std::vector& codeSection, const Domain::FileByteOffset codeSectionOffset, const Domain::FileByteOffset codeSectionSize) const { + if (codeSectionSize > codeSection.size()) throw CodegenException("Code section exceeds the input buffer", codeSectionOffset); + if (codeSectionSize > std::numeric_limits::max() - codeSectionOffset) throw CodegenException("Code section offset overflow", codeSectionOffset); + const auto limit = static_cast(codeSectionSize); const X64InstructionDecoder decoder; std::vector matches; std::size_t i = 0; + if (progress) progress(0, limit); + std::size_t lastReport = 0; while (i < limit) { const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; @@ -42,13 +43,18 @@ std::vector InstructionScanner::ScanCodeSection( matches.push_back({codeSectionOffset + i, length}); i += length; + if (progress && i - lastReport >= 65536) { + progress(i, limit); + lastReport = i; + } } + if (progress) progress(limit, limit); return matches; } -std::unique_ptr MakeInstructionScanner() { - return std::make_unique(); +std::unique_ptr MakeInstructionScanner(std::function progress) { + return std::make_unique(std::move(progress)); } } diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 4f9ff5b3b6..f905d2df43 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -369,6 +369,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( b == PrefixOperandSize || b == PrefixAddressSize) { if (b >= PrefixSegFs && b <= PrefixSegGs) info.SegmentPrefix = b; + if (b == PrefixAddressSize) + info.AddressSize32 = true; info.RexPrefix = 0; ++pos; continue; diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index b7e81a3076..5f1e25c734 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -14,6 +14,7 @@ struct GuestImport { std::uint64_t Addend; std::uint32_t RelocationType = 1; std::string Library; + bool Function = false; }; struct GuestRuntime { diff --git a/core/relinker/domain/include/domain/IProgress.hpp b/core/relinker/domain/include/domain/IProgress.hpp new file mode 100644 index 0000000000..27f40568da --- /dev/null +++ b/core/relinker/domain/include/domain/IProgress.hpp @@ -0,0 +1,28 @@ +#ifndef DOMAIN_IPROGRESS_HPP +#define DOMAIN_IPROGRESS_HPP + +#include +#include +#include + +namespace Domain { + +enum class ProgressStage { Executable, Modules, Syscalls, Decode, Lowering, Imports, Image, Write }; + +struct ProgressEvent { + ProgressStage Stage; + std::string Input; + std::uint64_t Completed; + std::uint64_t Total; +}; + +class IProgress { +public: + virtual ~IProgress() = default; + virtual void Report(const ProgressEvent& event) = 0; + virtual void Message(std::string_view message) = 0; +}; + +} + +#endif diff --git a/core/relinker/domain/include/domain/InputProgress.hpp b/core/relinker/domain/include/domain/InputProgress.hpp new file mode 100644 index 0000000000..185274552c --- /dev/null +++ b/core/relinker/domain/include/domain/InputProgress.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace Domain { + +class InputProgress final : public IProgress { +public: + InputProgress(IProgress& progress, std::string input) : progress(progress), input(std::move(input)) {} + + void Report(const ProgressEvent& event) override { + auto named = event; + if (named.Input.empty()) named.Input = input; + progress.Report(named); + } + + void Message(std::string_view message) override { progress.Message(message); } + +private: + IProgress& progress; + std::string input; +}; + +} diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 35ec257472..31405091c5 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -61,6 +61,8 @@ struct NidReference { FileByteOffset RelocationTableOffset; VirtualAddress RelocationAddress; std::int64_t Addend; + bool Weak = false; + bool Function = false; }; struct CallSiteInfo { @@ -97,6 +99,7 @@ struct SysVDynamicSection { std::vector RelaPltData; std::vector GuestModules; std::map ImportModules; + std::set FunctionImports; std::set UnmatchedImportModules; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 40802530ee..dc0099f1d1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -51,6 +51,7 @@ inline constexpr std::size_t kRelaPltAlignment = 8; inline constexpr std::size_t kGotAlignment = 8; inline constexpr std::size_t kGotReservedSlots = 3; +inline constexpr std::uint64_t kElfHeaderSize = 64; inline constexpr std::uint64_t kDefaultLoadAlignment = 0x1000; inline constexpr std::uint64_t kPhdrHeaderAlignment = 8; inline constexpr std::uint64_t kDynamicHeaderAlignment = 8; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp index 75b2ae1e17..fcdb2eca02 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IProgramHeaderLayoutBuilder.hpp @@ -13,6 +13,8 @@ namespace Elfpatcher { virtual std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const = 0; + virtual std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const = 0; + virtual std::uint16_t WriteLayout(std::vector& buf, const ProgramHeaderLayoutRequest& request) const = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp index 6c86cf4cf5..189256c86d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ProgramHeaderLayoutBuilder.hpp @@ -17,6 +17,8 @@ namespace Elfpatcher { std::uint64_t ComputeExtraBlockVaddr(const std::vector& originalHeaders, std::uint64_t extraBlockOffset) const override; + std::size_t RequiredProgramHeaderCount(const std::vector& originalHeaders) const override; + std::uint16_t WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request @@ -32,6 +34,7 @@ namespace Elfpatcher { [[nodiscard]] Domain::ProgramHeader _makeDynamicHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] Domain::ProgramHeader _makeInterpHeader(std::uint64_t offset, std::uint64_t vaddr, std::uint64_t size) const; [[nodiscard]] std::uint32_t _fixLoadFlags(std::uint32_t originalFlags) const; + [[nodiscard]] std::vector _keptHeaders(const std::vector& originalHeaders) const; void _sortLoadHeaders(std::vector& headers) const; void _writeProgramHeader(std::vector& buf, std::size_t offset, const Domain::ProgramHeader& ph) const; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 800fc04835..c9918ebb50 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -2,19 +2,28 @@ #define ELFPATCHER_WINDOWS_WINDOWSELFPATCHER_HPP #include +#include +#include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::filesystem::path iconPath = {}); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); + const std::string& GetDiagnostics() const; std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; private: - bool _windowsGui; - std::filesystem::path _iconPath; + std::function progress; + bool windowsGui; + std::optional> icon; + std::string diagnostics; + std::filesystem::path iconName; + std::optional fixedBase; + bool traceImports; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp index 94f47c6a31..7a1968e9e1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsEntryStubBuilder.hpp @@ -20,7 +20,7 @@ struct WindowsEntryStub { class WindowsEntryStubBuilder { public: - WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}, const std::set& unmatchedImportModules = {}) const; + WindowsEntryStub Build(std::uint32_t dataRva, std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& guestModules = {}, const std::set& unmatchedImportModules = {}, bool traceImports = false) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp index 978458f4e4..cdc42b7c2d 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsIconResourceBuilder.hpp @@ -8,7 +8,7 @@ namespace Elfpatcher::Windows { class WindowsIconResourceBuilder { public: - PeDirectory Build(const std::filesystem::path& iconPath, std::vector& sections, std::uint32_t nextRva) const; + PeDirectory Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, std::uint32_t nextRva) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index 8bd9ff6ec7..dc6d8cf6e6 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -8,11 +8,12 @@ namespace Elfpatcher::Windows { class WindowsLoadImage { public: - WindowsLoadImage(const std::vector& source, const std::vector& headers, bool requireEntry = true); + WindowsLoadImage(const std::vector& source, const std::vector& headers, bool requireEntry = true, std::uint64_t imageBase = ImageBase); std::uint32_t GetRva(std::uint64_t address, std::uint64_t size = 1) const; std::uint32_t GetEndRva() const; std::uint32_t GetEntryRva() const; std::uint64_t GetRelocatedAddress(std::uint64_t address) const; + std::uint64_t GetImageBase() const; void WritePointer(std::uint64_t address, std::uint64_t value); void RequireWritable(std::uint64_t address, std::uint64_t size) const; std::vector BuildSections() const; @@ -22,6 +23,7 @@ private: std::vector data; std::vector pageFlags; std::uint64_t firstAddress; + std::uint64_t imageBase; std::uint32_t entryRva = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bef348f4a9..237b762c60 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -35,6 +35,8 @@ struct PeImport { std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; std::string Library; + bool Weak = false; + bool Function = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index 9692b150bc..9c0b8f38f6 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -2,12 +2,13 @@ #define ELFPATCHER_WINDOWS_PEWRITER_HPP #include +#include namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index e7dc1ed7dd..207ef639bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -2,12 +2,13 @@ #define ELFPATCHER_WINDOWS_WINDOWSTLSBUILDER_HPP #include +#include namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}) const; }; } diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index cd7ca36dfb..c02856a8f4 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -3,7 +3,7 @@ #include #include #include -#include + #include #include #include @@ -132,24 +132,29 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( return vaddr; } +std::vector ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector& originalHeaders) const { + std::vector kept; + for (const auto& ph : originalHeaders) { + if (!_segmentFilter->ShouldSkip(ph)) + kept.push_back(ph); + } + return kept; +} + +std::size_t ProgramHeaderLayoutBuilder::RequiredProgramHeaderCount(const std::vector& originalHeaders) const { + return _keptHeaders(originalHeaders).size() + kSyntheticProgramHeaderCount; +} + std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( std::vector& buf, const ProgramHeaderLayoutRequest& request ) const { - std::vector kept; - std::size_t frameCount = 0; - for (const auto& ph : request.OriginalHeaders) { - if (_segmentFilter->ShouldSkip(ph)) - continue; - kept.push_back(ph); - if (ph.Type == PT_GNU_EH_FRAME) - frameCount++; - } + auto kept = _keptHeaders(request.OriginalHeaders); + const auto frameCount = std::count_if(kept.begin(), kept.end(), [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); const bool keepFrames = kept.size() + kSyntheticProgramHeaderCount <= request.PhNum; if (!keepFrames && frameCount > 0) { - std::erase_if(kept, [](const Domain::ProgramHeader& ph) { return ph.Type == PT_GNU_EH_FRAME; }); - std::cerr << "WARNING: No free program header slot for PT_GNU_EH_FRAME; C++ exceptions thrown in the executable cannot be caught.\n"; + throw Domain::RelinkerException("No free program header slot for PT_GNU_EH_FRAME"); } _sortLoadHeaders(kept); @@ -166,9 +171,13 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( }; if (request.PhEntSize < 56) throw Domain::RelinkerException("Program header entry is too small"); - const std::uint64_t headerBlockSize = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); - if (headerBlockSize > buf.size()) + const std::uint64_t tableEnd = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); + if (tableEnd > buf.size()) throw Domain::RelinkerException("Program header table exceeds the output file"); + const bool tableInExtraBlock = request.PhOff >= request.ExtraBlockOffset; + if (tableInExtraBlock && tableEnd - request.ExtraBlockOffset > request.ExtraBlockSize) + throw Domain::RelinkerException("Program header table does not fit within the extra block"); + const std::uint64_t headerBlockSize = tableInExtraBlock ? kElfHeaderSize : tableEnd; if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) throw Domain::RelinkerException("Extra block exceeds the output file"); std::uint64_t headerBlockAlign = kDefaultLoadAlignment; @@ -195,7 +204,8 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( const std::uint64_t interpVaddr = request.ExtraBlockVaddr + (request.InterpOffset - request.ExtraBlockOffset); std::vector layout; - layout.push_back(_makePhdrHeader(request.PhOff, headerBlockVaddr + request.PhOff, static_cast(neededPh) * request.PhEntSize)); + const std::uint64_t phdrVaddr = tableInExtraBlock ? request.ExtraBlockVaddr + (request.PhOff - request.ExtraBlockOffset) : headerBlockVaddr + request.PhOff; + layout.push_back(_makePhdrHeader(request.PhOff, phdrVaddr, static_cast(neededPh) * request.PhEntSize)); layout.push_back(_makeInterpHeader(request.InterpOffset, interpVaddr, request.InterpSize)); for (auto ph : kept) { if (ph.Type == PT_LOAD) diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index 916a1575eb..a7c4cc1f0f 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -80,7 +80,7 @@ void SectionHeaderTableBuilder::WriteTable( buf.resize(buf.size() + kShdrEntrySize * kSectionHeaderCount, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 0, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, shStrTabOff, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, 0, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, dynStrAddress, request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, dynSymAddress, request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, dynamicAddress, request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 1db4b27413..4fd051eba5 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -187,12 +187,22 @@ std::vector LinuxElfPatcher::Patch( for (char i : kInterp) buf.push_back(static_cast(i)); + std::uint64_t tableOff = phOff; + std::uint16_t tableSlots = phNum; + if (const auto required = _programHeaderLayoutBuilder->RequiredProgramHeaderCount(originalHeaders); required > phNum && required <= std::numeric_limits::max()) { + alignBuf(buf, kPhdrHeaderAlignment); + tableOff = static_cast(buf.size()); + tableSlots = static_cast(required); + buf.resize(buf.size() + required * phEntSize); + _byteWriter->WriteU64(buf, kEhdrPhOffOffset, tableOff); + } + const std::uint64_t extraBlockSize = static_cast(buf.size()) - extraBlockOff; ProgramHeaderLayoutRequest layoutRequest{}; - layoutRequest.PhOff = phOff; + layoutRequest.PhOff = tableOff; layoutRequest.PhEntSize = phEntSize; - layoutRequest.PhNum = phNum; + layoutRequest.PhNum = tableSlots; layoutRequest.OriginalHeaders = originalHeaders; layoutRequest.ExtraBlockOffset = extraBlockOff; layoutRequest.ExtraBlockVaddr = extraBlockVaddr; diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index a5ea5b7e3a..e7b753b724 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -66,7 +67,7 @@ std::u16string toUtf16(const std::string& text) { } -WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules, const std::set& unmatchedImportModules) const { +WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, const std::uint32_t entryRva, const WindowsImports& nativeImports, const std::vector& libraries, const std::vector& imports, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& guestModules, const std::set& unmatchedImportModules, const bool traceImports) const { if (!imports.empty() && libraries.empty()) throw Domain::RelinkerException("ELF imports have no DT_NEEDED libraries"); const auto path = normalizeRunPath(runPath); @@ -102,6 +103,9 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto argumentCount = reserve(4); const auto wideArguments = reserve(8); const auto shellHandle = reserve(8); + const auto logHandle = reserve(8); + const std::u16string logFileName = u"anyps5.log"; + const auto logName = addWideString(logFileName); const auto shellLibrary = addString("shell32.dll"); const auto parseArguments = addString("CommandLineToArgvW"); const auto handles = reserve(libraries.size() * 8); @@ -184,6 +188,24 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (unmatched.contains(stem(libraries[index]))) imported[index] = true; } + std::optional traceProvider; + std::map traceLibraries; + std::uint32_t traceSymbol = 0; + std::uint32_t traceFunction = 0; + if (traceImports) { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx") traceProvider = index; + } + if (!traceProvider.has_value()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); + imported[*traceProvider] = true; + traceSymbol = addString("TraceImport_nid_no_patch"); + traceFunction = reserve(8); + for (const auto& import : imports) { + if (!traceLibraries.contains(import.Library)) traceLibraries.emplace(import.Library, addString(import.Library)); + } + } + const auto lastError = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); @@ -212,6 +234,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con errors.push_back("FAIL: cannot prepare command-line arguments\n"); const auto directoryError = errors.size(); errors.push_back("FAIL: cannot make the executable directory the current directory\n"); + const auto traceError = errors.size(); + errors.push_back("FAIL: libc.prx does not export TraceImport_nid_no_patch\n"); std::vector errorRvas; for (const auto& error : errors) errorRvas.push_back(addString(error)); @@ -327,6 +351,48 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(converted, code.GetRva()); }; + const auto logMissingHandle = [&](const std::uint32_t standardHandle) { + const auto units = CheckedRva(logFileName.size() + 1); + code.Emit({0xb9}); + code.U32(standardHandle); + call("GetStdHandle"); + code.Emit({0x48, 0x85, 0xc0}); + const auto missing = code.Branch({0x0f, 0x84}); + code.Emit({0x48, 0x83, 0xf8, 0xff}); + const auto present = code.Branch({0x0f, 0x85}); + code.PatchBranch(missing, code.GetRva()); + code.Rip({0x48, 0x8b, 0x05}, logHandle); + code.Emit({0x48, 0x85, 0xc0}); + const auto opened = code.Branch({0x0f, 0x85}); + code.Emit({0x4c, 0x89, 0xe0, 0x48, 0x29, 0xd8, 0x48, 0x05}); + code.U32(units * 2); + code.Emit({0x48, 0x3d}); + code.U32(PathCapacity * 2); + const auto tooLong = code.Branch({0x0f, 0x87}); + code.Emit({0x4c, 0x89, 0xe7}); + code.Rip({0x48, 0x8d, 0x35}, logName); + code.Emit({0xb9}); + code.U32(units); + code.Emit({0x66, 0xf3, 0xa5}); + code.Rip({0x48, 0x8d, 0x0d}, modulePath); + code.Emit({0xba, 0, 0, 0, 0x40, 0x41, 0xb8, 3, 0, 0, 0, 0x45, 0x31, 0xc9}); + code.Emit({0x48, 0xc7, 0x44, 0x24, 0x20, 2, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x28, 0x80, 0, 0, 0, 0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0}); + call("CreateFileW"); + code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); + code.Emit({0x48, 0x83, 0xf8, 0xff}); + const auto failed = code.Branch({0x0f, 0x84}); + code.Rip({0x48, 0x89, 0x05}, logHandle); + code.PatchBranch(opened, code.GetRva()); + code.Emit({0xb9}); + code.U32(standardHandle); + code.Emit({0x48, 0x89, 0xc2}); + call("SetStdHandle"); + const auto done = code.GetRva(); + code.PatchBranch(present, done); + code.PatchBranch(tooLong, done); + code.PatchBranch(failed, done); + }; + code.Emit({0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x48, 0x83, 0xec, 0x60}); code.Emit({0x31, 0xc9}); code.Rip({0x48, 0x8d, 0x15}, programPath); @@ -353,6 +419,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.Rip({0x0f, 0x85}, findSeparator); code.Emit({0x49, 0x83, 0xc4, 0x02}); code.Emit({0x66, 0x41, 0xc7, 0x04, 0x24, 0x00, 0x00}); + logMissingHandle(0xfffffff5u); + logMissingHandle(0xfffffff4u); code.Rip({0x48, 0x8d, 0x0d}, modulePath); call("SetCurrentDirectoryW"); requireNonzero(directoryError, 0xc000000du); @@ -389,20 +457,31 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con writeString(resolvedPaths[index]); } + std::vector missingOptional; + std::optional probeFailed; + if (index >= guestModules.size() && !imported[index]) { + code.Emit({0x48, 0x89, 0xf1}); + call("GetFileAttributesW"); + code.Emit({0x83, 0xf8, 0xff}); + const auto exists = code.Branch({0x0f, 0x85}); + captureLastError(); + code.Emit({0x83, 0xf8, 2}); + missingOptional.push_back(code.Branch({0x0f, 0x84})); + code.Emit({0x83, 0xf8, 3}); + missingOptional.push_back(code.Branch({0x0f, 0x84})); + probeFailed = code.Branch({0xe9}); + code.PatchBranch(exists, code.GetRva()); + } + code.Emit({0x48, 0x89, 0xf1, 0x31, 0xd2, 0x41, 0xb8, 0, 0x11, 0, 0}); call("LoadLibraryExW"); code.Emit({0x48, 0x85, 0xc0}); const auto loadSucceeded = code.Branch({0x0f, 0x85}); captureLastError(); - std::optional unusedMissing; - if (index >= guestModules.size() && !imported[index]) { - code.Rip({0x8b, 0x05}, lastError); - code.Emit({0x83, 0xf8, 0x7e}); - unusedMissing = code.Branch({0x0f, 0x84}); - } if (dependencyDiagnostics) { writeString(loadFailed, true); } + if (probeFailed) code.PatchBranch(*probeFailed, code.GetRva()); writeString(failedModule, true); writeString(resolvedPaths[index], true); writeString(newline, true); @@ -419,14 +498,29 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (dependencyDiagnostics) { writeString(loaded); } - if (unusedMissing) { + if (!missingOptional.empty()) { const auto next = code.Branch({0xe9}); - code.PatchBranch(*unusedMissing, code.GetRva()); + for (const auto branch : missingOptional) code.PatchBranch(branch, code.GetRva()); if (dependencyDiagnostics) writeString(skippedModule, true); code.PatchBranch(next, code.GetRva()); } } + if (traceImports) { + code.Rip({0x48, 0x8b, 0x0d}, CheckedRva(handles + *traceProvider * 8)); + code.Rip({0x48, 0x8d, 0x15}, traceSymbol); + call("GetProcAddress"); + requireNonzero(traceError, 0xc0000139u); + code.Rip({0x48, 0x89, 0x05}, traceFunction); + } + const auto trace = [&](const PeImport& import, const std::uint32_t symbolName) { + if (!traceImports || !import.Function || import.Addend != 0) return; + code.Emit({0x48, 0x89, 0xc1}); + code.Rip({0x48, 0x8d, 0x15}, symbolName); + code.Rip({0x4c, 0x8d, 0x05}, traceLibraries.at(import.Library)); + code.Rip({0xff, 0x15}, traceFunction); + }; + std::vector unresolvedBranches; std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; @@ -449,6 +543,21 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto resolved = code.Branch({0x0f, 0x85}); code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); + if (imports[index].Weak) { + code.Emit({0x31, 0xc0}); + const auto writeWeak = code.GetRva(); + if (imports[index].Addend != 0) { + code.Emit({0x48, 0xba}); + code.U64(imports[index].Addend); + code.Emit({0x48, 0x01, 0xd0}); + } + guestStartup.WriteImport(code, imports[index], handles); + const auto weakDone = code.Branch({0xe9}); + code.PatchBranch(resolved, code.GetRva()); + code.Rip({0xe9}, writeWeak); + code.PatchBranch(weakDone, code.GetRva()); + continue; + } if (lazyBinding) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); @@ -473,6 +582,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.U64(imports[index].Addend); code.Emit({0x48, 0x01, 0xd0}); } + trace(imports[index], symbolNames[index]); guestStartup.WriteImport(code, imports[index], handles); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index bcc3469ddc..2e621c7cb0 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -70,7 +70,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else { if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library}); + runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library, type == 7 || (type == 6 && (symbol.Info & 15) == 2)}); } } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 6d59474ec5..aabd25ac42 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -3,7 +3,7 @@ #include #include #include -#include + #include namespace Elfpatcher::Windows { @@ -30,6 +30,9 @@ void validatePng(const std::vector& bytes, const std::filesystem:: ((color == 2 || color == 4 || color == 6) && (depth == 8 || depth == 16)); if (!validDepth || bytes[26] != 0 || bytes[27] != 0 || bytes[28] > 1) fail("invalid IHDR encoding"); bool hasData = false; + bool dataStarted = false; + bool dataEnded = false; + bool hasPalette = false; for (std::size_t offset = 8; offset < bytes.size();) { if (bytes.size() - offset < 12) fail("truncated chunk"); const auto length = readBigEndian(bytes, offset); @@ -42,7 +45,20 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } if ((crc ^ 0xffffffffu) != readBigEndian(bytes, offset + 8 + length)) fail("invalid chunk CRC"); if (type == 0x49484452 && offset != 8) fail("duplicate IHDR"); - if (type == 0x49444154 && length != 0) hasData = true; + if (type == 0x504c5445) { + if (hasPalette || dataStarted || !length || length % 3 || length > 768 || color == 0 || color == 4 || + (color == 3 && length / 3 > (1u << depth))) fail("invalid PLTE"); + hasPalette = true; + } + if (type == 0x49444154) { + if (dataEnded || (color == 3 && !hasPalette)) fail("invalid IDAT order or missing PLTE"); + dataStarted = true; + if (length != 0) hasData = true; + } else { + if (dataStarted) dataEnded = true; + if (!(type & 0x20000000u) && type != 0x49484452 && type != 0x504c5445 && type != 0x49454e44) + fail("unknown critical chunk"); + } offset += length + 12; if (type == 0x49454e44) { if (length != 0 || offset != bytes.size() || !hasData) fail("invalid IEND or missing IDAT"); @@ -54,19 +70,8 @@ void validatePng(const std::vector& bytes, const std::filesystem:: } -PeDirectory WindowsIconResourceBuilder::Build(const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { - if (iconPath.empty()) return {}; - std::error_code error; - const bool exists = std::filesystem::exists(iconPath, error); - if (error) throw Domain::RelinkerException("Cannot inspect Windows icon '" + iconPath.string() + "': " + error.message()); - if (!exists) return {}; - std::ifstream stream(iconPath, std::ios::binary | std::ios::ate); - if (!stream) throw Domain::RelinkerException("Cannot open Windows icon '" + iconPath.string() + "'"); - const auto size = stream.tellg(); - if (size < 0 || static_cast(size) > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon file size: " + iconPath.string()); - std::vector png(static_cast(size)); - stream.seekg(0); - if (!stream.read(reinterpret_cast(png.data()), size)) throw Domain::RelinkerException("Cannot read Windows icon '" + iconPath.string() + "'"); +PeDirectory WindowsIconResourceBuilder::Build(const std::vector& png, const std::filesystem::path& iconPath, std::vector& sections, const std::uint32_t nextRva) const { + if (png.size() > 0x7fffffffu - 184) throw Domain::RelinkerException("Invalid Windows icon size"); validatePng(png, iconPath); std::vector data(184 + png.size()); CheckedRva(static_cast(nextRva) + data.size()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 30cb39ecdf..71ce4f2bad 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { WindowsImports WindowsImportBuilder::Build(const std::uint32_t sectionRva) const { - const std::vector names = {"ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; + const std::vector names = {"CreateFileW", "ExitProcess", "FormatMessageA", "FreeLibrary", "GetCommandLineW", "GetFileAttributesW", "GetLastError", "GetModuleFileNameW", "GetModuleHandleA", "GetModuleHandleW", "GetProcAddress", "GetStdHandle", "GetSystemDirectoryW", "LoadLibraryExA", "LoadLibraryExW", "LocalFree", "MultiByteToWideChar", "RaiseException", "SetCurrentDirectoryW", "SetStdHandle", "VirtualAlloc", "WideCharToMultiByte", "WriteFile", "lstrcatA", "lstrcmpA", "lstrcmpiA", "lstrcpyA", "lstrlenA"}; WindowsImports result{{".idata", sectionRva, SectionRead | SectionWrite | 0x40u, std::vector(40)}, {sectionRva, 40}, {}, {}}; auto& bytes = result.Section.Data; const auto lookupOffset = bytes.size(); diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index f82ee3d930..ab78c51b1a 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry) { +WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry, const std::uint64_t imageBase) : imageBase(imageBase) { if (source.size() < 64 || Io::ReadU32(source, 0) != 0x464c457f || source[4] != 2 || source[5] != 1 || source[6] != 1 || Io::ReadU16(source, 18) != 62) throw Domain::RelinkerException("Windows output requires a little-endian ELF64 x86-64 image"); const auto type = Io::ReadU16(source, 16); @@ -78,7 +78,11 @@ std::uint32_t WindowsLoadImage::GetEntryRva() const { std::uint64_t WindowsLoadImage::GetRelocatedAddress(const std::uint64_t address) const { if (address < firstAddress || address - firstAddress > data.size()) throw Domain::RelinkerException("Relative relocation addend is outside the ELF image", address); - return ImageBase + LoadRva + address - firstAddress; + return imageBase + LoadRva + address - firstAddress; +} + +std::uint64_t WindowsLoadImage::GetImageBase() const { + return imageBase; } void WindowsLoadImage::WritePointer(const std::uint64_t address, const std::uint64_t value) { diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 3ac43727d4..2b4faa6bf5 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -8,32 +8,19 @@ #include #include #include -#include -#include + + #include namespace Elfpatcher::Windows { namespace { -void writeDiagnosticsImports(const std::vector& imports) { - const std::filesystem::path path = std::filesystem::absolute("windows-diagnostics-imports.txt"); - std::ofstream stream(path, std::ios::trunc); - if (!stream) - throw Domain::RelinkerException("Cannot open windows-diagnostics-imports.txt for writing"); - for (const auto& import : imports) - stream << import.Name << '\n'; - if (!stream) - throw Domain::RelinkerException("Cannot write windows-diagnostics-imports.txt"); - stream.close(); - std::cout << "Wrote Windows import diagnostics to " << path.string() << '\n'; -} - -void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva) { +void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) { for (auto& section : sections) { if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8) continue; - Io::WriteU64(section.Data, targetRva - section.Rva, ImageBase + stubRva); + Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva); return; } throw Domain::RelinkerException("Lazy import GOT slot is not contained in any section", targetRva); @@ -41,16 +28,26 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::filesystem::path iconPath) : _windowsGui(windowsGui), _iconPath(std::move(iconPath)) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { + if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) + throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } +const std::string& WindowsPePatcher::GetDiagnostics() const { return diagnostics; } + std::vector WindowsPePatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { - WindowsLoadImage image(sourceElf, originalHeaders); + const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; + report(0); + diagnostics.clear(); + WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - LoadRva : ImageBase); if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); + report(500); const WindowsRelocationBuilder relocationBuilder; auto relocations = relocationBuilder.Apply(image, dynamicSection); + report(1000); auto sections = image.BuildSections(); + report(1500); std::array directories{}; auto nextRva = image.GetEndRva(); bool hasProcessParameters = false; @@ -71,7 +68,8 @@ std::vector WindowsPePatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType, import.Library}); + for (const auto& import : module.Imports) { + relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType, import.Library}); + relocations.Imports.back().Function = import.Function; + } } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) - writeDiagnosticsImports(relocations.Imports); - auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules, dynamicSection.UnmatchedImportModules); + for (const auto& import : relocations.Imports) diagnostics += import.Name + '\n'; + report(8500); + auto entry = WindowsEntryStubBuilder().Build(nextRva, image.GetEntryRva(), nativeImports, libraries, relocations.Imports, runPath, lazyBinding, dependencyDiagnostics, dynamicSection.GuestModules, dynamicSection.UnmatchedImportModules, traceImports); + report(9000); directories[3] = entry.ExceptionDirectory; const auto entryRva = entry.Code.Rva; nextRva = AlignRva(entry.Code.Rva + entry.Code.Data.size()); @@ -96,17 +99,20 @@ std::vector WindowsPePatcher::Patch(const std::vector{} : relocationBuilder.BuildBaseRelocations(relocations.BaseRelocations); if (!relocationData.empty()) { directories[5] = {nextRva, CheckedRva(relocationData.size())}; sections.push_back({".reloc", nextRva, SectionRead | 0x02000040u, std::move(relocationData)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } - directories[2] = WindowsIconResourceBuilder().Build(_iconPath, sections, nextRva); - return WindowsPeWriter().Write(sections, entryRva, directories, _windowsGui); + directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, iconName, sections, nextRva) : PeDirectory{}; + report(9500); + auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional(image.GetImageBase()) : std::nullopt); + report(10000); + return result; } } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index e91f5e59f5..1e346dbeda 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::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional fixedImageBase) const { constexpr std::size_t peOffset = 0x80; constexpr std::size_t optionalOffset = peOffset + 24; constexpr std::size_t sectionTable = optionalOffset + 240; @@ -18,10 +18,12 @@ std::vector WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, peOffset + 4, 0x8664); Io::WriteU16(result, peOffset + 6, static_cast(sections.size())); Io::WriteU16(result, peOffset + 20, 240); - Io::WriteU16(result, peOffset + 22, 0x22); + if (fixedImageBase && (directories[5].Size != 0 || *fixedImageBase < 0x10000 || (*fixedImageBase & 0xffff) != 0)) + throw Domain::RelinkerException("Invalid fixed Windows image base or base relocations"); + Io::WriteU16(result, peOffset + 22, fixedImageBase ? 0x23 : 0x22); Io::WriteU16(result, optionalOffset, 0x20b); Io::WriteU32(result, optionalOffset + 16, entryRva); - Io::WriteU64(result, optionalOffset + 24, ImageBase); + Io::WriteU64(result, optionalOffset + 24, fixedImageBase.value_or(ImageBase)); Io::WriteU32(result, optionalOffset + 32, SectionAlignment); Io::WriteU32(result, optionalOffset + 36, FileAlignment); Io::WriteU16(result, optionalOffset + 40, 6); @@ -70,6 +72,8 @@ std::vector WindowsPeWriter::Write(const std::vector& s } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); + if (fixedImageBase && *fixedImageBase > 0x7fffffffffff - static_cast(endRva)) + throw Domain::RelinkerException("Fixed Windows image exceeds the user address space"); Io::WriteU32(result, optionalOffset + 4, codeSize); Io::WriteU32(result, optionalOffset + 8, dataSize); Io::WriteU32(result, optionalOffset + 56, endRva); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 9706bfd1b7..f1b5036de8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -50,6 +50,8 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; + result.Imports.back().Function = type == 7 || (type == 6 && dynamicSection.FunctionImports.contains(target)); if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index e37c631dcc..2a017013c4 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -92,13 +92,15 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva) const { +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { + const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; std::vector accesses; std::set branchTargets; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); + std::uint64_t scanned = 0; for (const auto& header : headers) { if (header.Type == 7) { @@ -110,6 +112,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co continue; std::uint64_t decodedEnd = 0; for (auto instruction = instructions.lower_bound(header.MappedAddress); instruction != instructions.end() && *instruction - header.MappedAddress < header.FileSize; ++instruction) { + if (scanned % 4096 == 0) report(7000 + 2000 * scanned / std::max(instructions.size(), 1)); + ++scanned; const auto offset = *instruction - header.MappedAddress; const auto* bytes = source.data() + header.Offset + offset; const auto info = decoder.DecodeInstruction(bytes, header.FileSize - offset); @@ -187,6 +191,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } } + report(9000); if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); @@ -238,7 +243,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.PatchBranch(skipCallback, code.GetRva()); code.Emit({0xc3}); + std::uint64_t patched = 0; for (const auto& access : accesses) { + if (patched % 256 == 0) report(9000 + 900 * patched / accesses.size()); + ++patched; const auto target = branchTargets.upper_bound(access.Rva); if (target != branchTargets.end() && *target < access.Rva + access.Length) throw Domain::RelinkerException("Branch enters a guest TLS instruction", *target); @@ -390,10 +398,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co code.Rip({0xe9}, CheckedRva(access.Rva + access.Length)); } + report(9900); const auto templateBytes = WindowsTlsTemplateBuilder().Build(*tls, image, sections, templateRva, relocations); std::copy(templateBytes.begin(), templateBytes.end(), data.Data.begin() + templateOffset); const auto writeAddress = [&](const std::size_t offset, const std::uint32_t rva) { - Io::WriteU64(data.Data, offset, ImageBase + rva); + Io::WriteU64(data.Data, offset, image.GetImageBase() + rva); relocations.push_back(CheckedRva(data.Rva + offset)); }; diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp index 5332f80e58..7f718b66ce 100644 --- a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -21,6 +21,10 @@ int main() { const Elfpatcher::SectionHeaderTableRequest valid{128, 8, 136, 24, 160, 16, 176, 16}; auto bytes = Fixture(); builder.WriteTable(bytes, valid); const auto table = Io::ReadU64(bytes, 40); + Require(Io::ReadU64(bytes, table + 64 + 8) == 0); + Require(Io::ReadU64(bytes, table + 64 + 16) == 0); + Require(Io::ReadU64(bytes, table + 64 + 24) == 512); + Require(Io::ReadU64(bytes, table + 64 + 32) != 0); Require(Io::ReadU64(bytes, table + 2 * 64 + 16) == 0x1000); auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; bytes = Fixture(); builder.WriteTable(bytes, empty); diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index f91bf45f6d..772189083e 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -4,9 +4,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -29,31 +27,80 @@ std::uint32_t readU32(const std::vector& data, const std::size_t o static_cast(data[offset + 2]) << 16 | static_cast(data[offset + 3]) << 24; } -void writeBytes(const std::filesystem::path& path, const std::vector& bytes) { - std::filesystem::create_directories(path.parent_path()); - std::ofstream stream(path, std::ios::binary); - stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - require(static_cast(stream), "Cannot write " + path.string()); +std::vector chunk(const std::string& type, const std::vector& payload) { + std::vector result; + const auto append = [&result](std::uint32_t value) { + for (const auto shift : {24, 16, 8, 0}) result.push_back(static_cast(value >> shift)); + }; + append(static_cast(payload.size())); + result.insert(result.end(), type.begin(), type.end()); + result.insert(result.end(), payload.begin(), payload.end()); + std::uint32_t crc = 0xffffffffu; + for (std::size_t index = 4; index < result.size(); ++index) { + crc ^= result[index]; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320u : 0u); + } + append(crc ^ 0xffffffffu); + return result; } -void requireEmpty(const Elfpatcher::Windows::WindowsIconResourceBuilder& builder, const std::filesystem::path& path, const std::string& name) { - std::vector sections; - const auto directory = builder.Build(path, sections, 0x10000); - require(sections.empty(), "Section added for " + name); - require(directory.Rva == 0 && directory.Size == 0, "Resource directory set for " + name); +void checkChunks() { + const Elfpatcher::Windows::WindowsIconResourceBuilder builder; + const auto check = [&](const std::string& name, const std::vector>& chunks, bool valid, const std::vector& header = {}) { + std::vector image(Png.begin(), Png.begin() + (header.empty() ? 33 : 8)); + if (!header.empty()) { + const auto ihdr = chunk("IHDR", header); + image.insert(image.end(), ihdr.begin(), ihdr.end()); + } + for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); + image.insert(image.end(), Png.end() - 12, Png.end()); + const std::filesystem::path path = std::filesystem::path(name) / "icon.png"; + std::vector sections; + bool rejected = false; + try { builder.Build(image, path, sections, 0x10000); } + catch (const Domain::RelinkerException& error) { + rejected = true; + require(std::string(error.what()).find(path.string()) != std::string::npos, "Icon failure omitted the path"); + } + require(rejected != valid, "Unexpected validation result for " + name); + require(sections.size() == (valid ? 1u : 0u), "Unexpected section mutation for " + name); + }; + const std::vector data(Png.begin() + 41, Png.begin() + 54); + const auto idat = chunk("IDAT", data); + const auto note = chunk("tEXt", {'n', 0, 'v'}); + check("unknown-critical", {chunk("ABCD", {}), idat}, false); + check("ancillary", {note, idat}, true); + const auto first = chunk("IDAT", {data.begin(), data.begin() + 6}); + const auto last = chunk("IDAT", {data.begin() + 6, data.end()}); + check("consecutive", {first, last}, true); + check("nonconsecutive", {first, note, last}, false); + const auto palette = chunk("PLTE", {255, 0, 0}); + check("late-palette", {idat, palette}, false); + check("duplicate-palette", {palette, palette, idat}, false); + check("palette", {palette, idat}, true); + check("empty-palette", {chunk("PLTE", {}), idat}, false); + check("partial-palette", {chunk("PLTE", {0, 1, 2, 3}), idat}, false); + check("large-palette", {chunk("PLTE", std::vector(771)), idat}, false); + std::vector header(Png.begin() + 16, Png.begin() + 29); + header[8] = 1; + header[9] = 3; + const auto indexed = chunk("IDAT", {0x78, 0x9c, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01}); + check("indexed", {palette, indexed}, true, header); + check("indexed-missing-palette", {indexed}, false, header); + check("indexed-large-palette", {chunk("PLTE", std::vector(9)), indexed}, false, header); + header[9] = 0; + check("grayscale-palette", {palette, indexed}, false, header); } -void run(const std::filesystem::path& root) { +void run() { const Elfpatcher::Windows::WindowsIconResourceBuilder builder; - requireEmpty(builder, {}, "an empty path"); - requireEmpty(builder, root / "missing" / "icon0.png", "a missing icon"); + checkChunks(); - const auto iconPath = root / "valid" / "icon0.png"; - writeBytes(iconPath, Png); + const std::filesystem::path iconPath = "valid/icon0.png"; const std::uint32_t nextRva = 0x50000; std::vector sections; - const auto directory = builder.Build(iconPath, sections, nextRva); + const auto directory = builder.Build(Png, iconPath, sections, nextRva); require(sections.size() == 1, "Expected one section"); require(sections[0].Name == ".rsrc", "Unexpected section name: " + sections[0].Name); require(sections[0].Rva == nextRva, "Unexpected section RVA"); @@ -68,13 +115,12 @@ void run(const std::filesystem::path& root) { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = root / "bad" / "icon0.png"; + const auto badPath = std::filesystem::path("bad/icon0.png"); auto truncated = Png; truncated.resize(40); - writeBytes(badPath, truncated); std::vector badSections; try { - builder.Build(badPath, badSections, nextRva); + builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); require(badSections.empty(), "Section added despite the failure"); @@ -86,15 +132,11 @@ void run(const std::filesystem::path& root) { } int main() { - const auto root = std::filesystem::temp_directory_path() / ("anyps5-icon-resource-tests-" + std::to_string(std::random_device{}())); - std::filesystem::remove_all(root); try { - run(root); + run(); } catch (const std::exception& e) { std::cerr << e.what() << '\n'; - std::filesystem::remove_all(root); return 1; } - std::filesystem::remove_all(root); return 0; } diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrt.def b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def new file mode 100644 index 0000000000..c163db8f44 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrt.def @@ -0,0 +1,4 @@ +LIBRARY msvcrt.dll +EXPORTS + __iob_func + fputs diff --git a/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp new file mode 100644 index 0000000000..4c370e0924 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsMsvcrtStdioHost.cpp @@ -0,0 +1,22 @@ +struct _iobuf { + char* ptr; + int count; + char* base; + int flags; + int file; + int charbuf; + int bufferSize; + char* tempName; +}; + +static_assert(sizeof(_iobuf) == 48); + +extern "C" _iobuf* __cdecl __iob_func(); +extern "C" int __cdecl fputs(const char*, _iobuf*); + +extern "C" int shared() { + auto* streams = __iob_func(); + fputs("Windows DLL stdout probe\n", streams + 1); + fputs("Windows DLL stderr probe\n", streams + 2); + return 11; +} diff --git a/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp new file mode 100644 index 0000000000..72a29373f4 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsStdioHost.cpp @@ -0,0 +1,7 @@ +#include + +extern "C" int shared() { + std::fputs("Windows DLL stdout probe\n", stdout); + std::fputs("Windows DLL stderr probe\n", stderr); + return 11; +} diff --git a/core/relinker/io/src/NativePath.cpp b/core/relinker/io/src/NativePath.cpp index 911b1026f0..13b11fa505 100644 --- a/core/relinker/io/src/NativePath.cpp +++ b/core/relinker/io/src/NativePath.cpp @@ -1,46 +1,15 @@ #include -#include - -#ifdef _WIN32 -#include -#include -#endif namespace Io { -#ifdef _WIN32 - std::filesystem::path NativePath(const std::string& path) { - if (path.empty()) return {}; - const int wideSize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), static_cast(path.size()), nullptr, 0); - if (wideSize <= 0) throw Domain::RelinkerException("Path is not valid UTF-8: " + path); - std::wstring wide(static_cast(wideSize), L'\0'); - if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), static_cast(path.size()), wide.data(), wideSize) != wideSize) - throw Domain::RelinkerException("Path is not valid UTF-8: " + path); - return std::filesystem::path(std::move(wide)); + const std::u8string utf8(path.begin(), path.end()); + return std::filesystem::path(utf8); } std::string Utf8Path(const std::filesystem::path& path) { - const std::wstring wide = path.wstring(); - if (wide.empty()) return {}; - const int utf8Size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), nullptr, 0, nullptr, nullptr); - if (utf8Size <= 0) throw Domain::RelinkerException("Path cannot be encoded as UTF-8"); - std::string utf8(static_cast(utf8Size), '\0'); - if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), utf8Size, nullptr, nullptr) != utf8Size) - throw Domain::RelinkerException("Path cannot be encoded as UTF-8"); - return utf8; + const auto utf8 = path.u8string(); + return std::string(utf8.begin(), utf8.end()); } -#else - -std::filesystem::path NativePath(const std::string& path) { - return std::filesystem::path(path); -} - -std::string Utf8Path(const std::filesystem::path& path) { - return path.string(); -} - -#endif - } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f67db4aa08..95652550c3 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -1,28 +1,14 @@ +#include +#include +#include +#include #include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include #include @@ -38,6 +24,50 @@ namespace { +struct ConvertedExecutable { + std::string InputPath; + std::string OutputPath; + Relinker::RelinkResult Result; + std::vector GuestArtifacts; + std::vector Bytes; + std::string Diagnostics; +}; + +ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, Cli::ConsoleProgress& progress) { + Io::FileReader fileReader; + Relinker::ConversionInput input; + input.Executable = fileReader.Read(inputPath); + input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); + input.OutputPath = Io::NativePath(absPath); + if (!args.skipSceModule) { + auto modules = Cli::ReadModules(input.InputPath, Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + input.ModuleRoot = std::move(modules.Root); + input.Modules = std::move(modules.Modules); + input.RuntimeDirectories = std::move(modules.RuntimeDirectories); + } + const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; + if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); + Relinker::ConversionOptions options; + options.Target = args.toWindows ? Relinker::TargetSystem::Windows : args.toMacos ? Relinker::TargetSystem::MacOs : Relinker::TargetSystem::Linux; + options.ToIntel = args.toIntel; + options.ToRosetta = args.toRosetta; + options.SkipSyscallCheck = args.skipSyscallCheck; + options.TrapSdkSyscallStubs = args.trapSdkSyscallStubs; + options.UnusedFilterLevel = args.unusedFilterLevel; + options.LazyBinding = args.lazyBinding; + options.RunPath = args.runPath; + options.WindowsGui = args.windowsGui; + options.TraceImports = args.traceImports; + options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt; + options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; + options.ExcludedModules = args.excludedSceModules; + progress.IncludeFile(Io::Utf8Path(input.InputPath)); + progress.IncludeFile(Io::Utf8Path(input.OutputPath)); + for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); + auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -52,78 +82,83 @@ int Run(const int argc, char* argv[]) { return 0; } + Cli::ConsoleProgress progress(std::cout); try { auto extension = Io::Utf8Path(Io::NativePath(args.outputPath).extension()); for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); if (!args.toWindows && !args.toMacos && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n"; - Io::FileReader fileReader; Io::FileWriter fileWriter; - auto sourceBytes = fileReader.Read(args.inputPath); - if (std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) + 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"); const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); - std::vector trampolines; - if (args.toIntel) { - const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); - auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); - sourceBytes = std::move(converted.Bytes); - trampolines = std::move(converted.Trampolines); - std::map stubsByName; - const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; - for (const auto& report : converted.Reports) { - if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (counted(report.InstructionName)) - ++stubsByName[report.InstructionName]; - else - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; + struct ExtraExecutable { + std::string Argument; + std::filesystem::path Relative; + std::filesystem::path Input; + std::filesystem::path Output; + }; + std::vector extraExecutables; + const auto outputDirectory = Io::NativePath(absPath).parent_path(); + const auto moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); + std::vector moduleDirectories; + for (const auto* name : {"sce_module", "sce_modules", "prx"}) moduleDirectories.push_back(moduleRoot / name); + for (const auto& name : args.moduleDirectories) moduleDirectories.push_back((moduleRoot / Io::NativePath(name)).lexically_normal()); + for (auto& directory : moduleDirectories) if (!directory.has_filename()) directory = directory.parent_path(); + for (const auto& executable : args.executables) { + const auto relative = Io::NativePath(executable).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == ".." || !relative.has_filename()) + throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executable); + const auto input = moduleRoot / relative; + if (!std::filesystem::is_regular_file(input)) throw Domain::RelinkerException("--executable is not a file: " + Io::Utf8Path(input)); + if (std::find(moduleDirectories.begin(), moduleDirectories.end(), input.parent_path()) != moduleDirectories.end()) + throw Domain::RelinkerException("--executable is in a guest module directory: " + executable); + std::string name; + for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part); + const auto output = outputDirectory / Io::NativePath(name + Io::Utf8Path(Io::NativePath(absPath).extension())); + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); + for (const auto& extra : extraExecutables) { + if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } - for (const auto& [name, count] : stubsByName) - std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; - std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; + extraExecutables.push_back({executable, relative, input, output}); } - auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs); - - const auto pipeline = std::make_shared( - elfReader, - syscallScanner, - Relinker::MakeCallSiteResolver(), - std::make_shared(), - std::make_shared(), - args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(), - args.unusedFilterLevel - ); - - std::cout << "System: " << (args.toWindows ? "Windows" : args.toMacos ? "macOS" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; - std::cout << Relinker::GuestModulePattern << " processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; - for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n'; - auto result = pipeline->Relink(sourceBytes); - for (const auto& patch : result.Patches) { - if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) - throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); - for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + progress.IncludeFile(args.inputPath); + progress.IncludeFile(absPath); + for (const auto& extra : extraExecutables) { + progress.IncludeFile(Io::Utf8Path(extra.Input)); + progress.IncludeFile(Io::Utf8Path(extra.Output)); } - - std::vector guestArtifacts; - if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(args.inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath), args.moduleDirectories); + std::vector executables; + executables.push_back(Convert(args, args.inputPath, absPath, true, progress)); + for (const auto& [executable, relative, input, output] : extraExecutables) { + std::cout << "Executable: " << relative.generic_string() << '\n'; + auto converted = Convert(args, Io::Utf8Path(input), Io::Utf8Path(output), false, progress); + const auto& mainArtifacts = executables.front().GuestArtifacts; + const bool shared = converted.GuestArtifacts.size() == mainArtifacts.size() && std::equal(mainArtifacts.begin(), mainArtifacts.end(), converted.GuestArtifacts.begin(), [](const auto& left, const auto& right) { + return left.Path == right.Path && left.Bytes == right.Bytes; + }); + if (!shared) throw Domain::RelinkerException("Guest modules convert differently for " + executable + ", so it cannot share them with the input executable"); + executables.push_back(std::move(converted)); } + const auto& guestArtifacts = executables.front().GuestArtifacts; const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { if (!std::filesystem::exists(path)) return; - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) - throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& executable : executables) + if (std::filesystem::equivalent(Io::NativePath(executable.InputPath), path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); for (const auto& artifact : guestArtifacts) if (std::filesystem::equivalent(artifact.SourcePath, path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); }; + for (std::size_t index = 1; index < executables.size(); ++index) checkOutputInputs(Io::NativePath(executables[index].OutputPath), "Executable"); if (desktopEntry) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); @@ -133,78 +168,76 @@ int Run(const int argc, char* argv[]) { std::vector registryPaths; if (args.writeRegistry) { + for (const auto& executable : executables) { + const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath); + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); + } const std::filesystem::path outFsPath = Io::NativePath(absPath); - registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); } - auto byteWriter = std::make_shared(); + checkOutputInputs(Io::NativePath(absPath), "Executable"); + for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); - std::shared_ptr patcher; - if (args.toMacos) { - patcher = std::make_shared(); - } else if (args.toWindows) { - patcher = std::make_shared(args.windowsGui, Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"); - } else { - patcher = std::make_shared( - std::make_shared(), - std::make_shared( - std::make_shared(), - byteWriter - ), - std::make_shared(byteWriter), - byteWriter - ); - } - - std::vector executableBytes; - try { - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics, trampolines); - } catch (Domain::RelinkerException& error) { - error.InputPath = args.inputPath; - throw; - } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); + const auto outputCount = guestArtifacts.size() + executables.size(); + progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); + std::size_t written = 0; for (const auto& artifact : guestArtifacts) { std::filesystem::create_directories(artifact.Path.parent_path()); fileWriter.Write(Io::Utf8Path(artifact.Path), artifact.Bytes); - std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; + progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); + } + for (const auto& executable : executables) { + fileWriter.Write(executable.OutputPath, executable.Bytes); + progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); } - fileWriter.Write(absPath, executableBytes); if (args.writeRegistry) { const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < executables.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries)); for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + fileWriter.Write(Io::Utf8Path(registryPaths[executables.size() + index]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } + progress.Finish(); if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; - std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; - std::cout << "Expected runtime layout (relative to the output executable):\n" - << Io::Utf8Path(Io::NativePath(absPath).filename()) << "\n" - << "libs/\n *.prx\napp0/\n \n"; + std::cout << "\nConversion complete\n" + << "Output file: " << absPath << '\n' + << "External prx references: " << executables.front().Result.RegistryEntries.size() << '\n' + << "Converted modules: " << guestArtifacts.size() << "\n\n"; + std::vector extraPaths; + for (std::size_t index = 1; index < executables.size(); ++index) { + std::cout << "Executable file: " << executables[index].OutputPath << '\n'; + extraPaths.push_back(Io::Utf8Path(Io::NativePath(executables[index].OutputPath).filename())); + } + std::vector modulePaths; for (const auto& artifact : guestArtifacts) { std::string relative = Io::Utf8Path(artifact.Path.lexically_relative(Io::NativePath(absPath).parent_path() / "app0")); std::replace(relative.begin(), relative.end(), '\\', '/'); - std::cout << " " << relative << '\n'; + modulePaths.push_back(std::move(relative)); } - std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; + Cli::RuntimeLayout::Print(std::cout, Io::Utf8Path(Io::NativePath(absPath).filename()), modulePaths, extraPaths); if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); } catch (const Domain::RelinkerException& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what(); if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; std::cerr << "\n"; if (!e.InputPath.empty()) std::cerr << "Input: " << e.InputPath << '\n'; return 2; } catch (const Codegen::CodegenException& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what(); if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; std::cerr << "\n"; return 2; } catch (const std::exception& e) { + progress.Finish(); std::cerr << "FAIL: " << e.what() << "\n"; return 2; } diff --git a/core/relinker/relinker.manifest b/core/relinker/relinker.manifest new file mode 100644 index 0000000000..8f006058f1 --- /dev/null +++ b/core/relinker/relinker.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + true + + + diff --git a/core/relinker/relinker.rc b/core/relinker/relinker.rc new file mode 100644 index 0000000000..a543e8bc25 --- /dev/null +++ b/core/relinker/relinker.rc @@ -0,0 +1,3 @@ +#include + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "relinker.manifest" diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index 2146ab9cd6..9169186da7 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}) const; }; } diff --git a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp index 29e14cf094..8b9c2d6c25 100644 --- a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp @@ -3,10 +3,11 @@ #include #include +#include namespace Relinker { -std::unique_ptr MakeSyscallScanner(bool trapSdkStubs = false); +std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, bool trapSdkStubs = false); std::unique_ptr MakeNullSyscallScanner(); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp index 91ca7f3065..acd0fc633f 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter.hpp @@ -3,11 +3,12 @@ #include #include +#include namespace Relinker { std::shared_ptr MakeUnusedNidFilter(); -std::shared_ptr MakeStrictUnusedNidFilter(); +std::shared_ptr MakeStrictUnusedNidFilter(Domain::IProgress& progress); } diff --git a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp index 7b1add380f..77e6466fc2 100644 --- a/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp +++ b/core/relinker/relinker/include/relinker/analysis/UnusedNidFilter/IControlFlowGraph.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -27,15 +28,7 @@ std::unique_ptr BuildControlFlowGraph( bool followCodeAddresses = false ); -std::unique_ptr BuildControlFlowGraph( - std::span text, - VirtualAddress textVaddr, - VirtualAddress entryVaddr, - const std::vector& extraEntries, - const IRelativeRelocationIndex& relativeRelocations, - bool followCodeAddresses, - std::unordered_set& reachable -); +std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress = {}); std::unique_ptr BuildControlFlowGraph( const std::vector& text, diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 021938a315..71a9160da0 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker { @@ -11,11 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls( - const std::vector& codeSection, - FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize - ) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index d566a152c4..bef96dbe10 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -67,9 +68,14 @@ struct GuestArtifact { std::filesystem::path SourcePath; }; +struct GuestModuleInput { + std::filesystem::path Path; + std::vector Bytes; +}; + class GuestModuleBuilder { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& sceModulePath, const std::vector& moduleDirectories = {}) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool macos, bool toIntel, bool toRosetta, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const; }; } diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp new file mode 100644 index 0000000000..d6b82a2261 --- /dev/null +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +namespace Relinker { + +enum class TargetSystem { Linux, Windows, MacOs }; + +struct ConversionOptions { + TargetSystem Target = TargetSystem::Linux; + std::uint32_t UnusedFilterLevel = 0; + bool ToIntel = false; + bool ToRosetta = false; + bool SkipSyscallCheck = false; + bool TrapSdkSyscallStubs = false; + bool LazyBinding = false; + bool WindowsGui = false; + bool TraceImports = false; + std::optional WindowsFixedBase; + bool WindowsDiagnostics = false; + std::string RunPath = "$ORIGIN/libs"; + std::set ExcludedModules; +}; + +struct ConversionInput { + std::vector Executable; + std::filesystem::path InputPath; + std::filesystem::path OutputPath; + std::filesystem::path ModuleRoot; + std::vector Modules; + std::vector RuntimeDirectories; + std::optional> Icon; +}; + +struct ConversionOutput { + std::vector Executable; + std::vector Modules; + RelinkResult Analysis; + std::string WindowsDiagnostics; +}; + +class Conversion { +public: + explicit Conversion(Domain::IProgress& progress); + ConversionOutput Convert(ConversionInput input, const ConversionOptions& options) const; + +private: + Domain::IProgress& progress; +}; + +} diff --git a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp index 78554168b1..43080f49af 100644 --- a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp @@ -9,25 +9,27 @@ #include #include #include +#include namespace Relinker { class RelinkerPipeline : public IRelinkerPipeline { public: - 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); + 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); RelinkResult Relink(const std::vector& sourceElf) override; private: - 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::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; - static std::string _relocationTypeName(std::uint32_t type); + static std::string relocationTypeName(std::uint32_t type); }; } diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 876fa53ea4..2fd1a9a2eb 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -34,7 +34,13 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress) const { + std::uint64_t lastProgress = 0; + const auto report = [&](std::uint64_t completed) { + if (completed <= lastProgress) return; + lastProgress = completed; + if (progress) progress(completed, 10000); + }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) throw Domain::RelinkerException("Code analysis: file range exceeds image", offset); }; @@ -223,6 +229,10 @@ std::set CodeInstructionCollector::Collect(const std::ve const Codegen::X64InstructionDecoder decoder; std::vector> skipped; std::set staticTargets; + std::uint64_t functionBytes = 0; + for (const auto& [begin, end] : functions) functionBytes += end - begin; + std::uint64_t decodedBytes = 0; + report(1000); for (const auto& [begin, end] : functions) { const auto offset = fileOffset(begin, end - begin); for (auto address = begin; address < end;) { @@ -253,8 +263,14 @@ std::set CodeInstructionCollector::Collect(const std::ve addRoot(target); } address += info.Length; + decodedBytes += info.Length; + if (progress && (decodedBytes & 65535) < info.Length) report(1000 + 2500 * decodedBytes / functionBytes); } } + report(3500); + std::uint64_t scanBytes = 0; + for (const auto& header : headers) if (header.Type == 1 && (header.Flags & 1) != 0) scanBytes += header.FileSize; + std::uint64_t scannedBytes = 0; for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || header.FileSize < 16) continue; range(header.Offset, header.FileSize); @@ -263,11 +279,15 @@ std::set CodeInstructionCollector::Collect(const std::ve const auto paddedStart = [&](std::size_t address) { return address % 16 == 0 && address >= 2 && text[address - 1] == 0xcc && text[address - 2] == 0xcc; }; const auto nextPaddedStart = [&](std::size_t address) { address = (address + 16) & ~std::size_t{15}; - while (address < size && !paddedStart(address)) address += 16; + while (address < size && !paddedStart(address)) { + if (progress && address % 65536 == 0) report(3500 + 2500 * (scannedBytes + address) / scanBytes); + address += 16; + } return address; }; bool afterTransfer = false; for (std::size_t position = nextPaddedStart(0); position < size;) { + if (progress && position % 65536 < 16) report(3500 + 2500 * (scannedBytes + position) / scanBytes); const auto aligned = (position + 15) & ~std::size_t{15}; const bool zeroPadding = afterTransfer && aligned != position && aligned <= size && std::all_of(text + position, text + aligned, [](const std::uint8_t value) { return value == 0; }); afterTransfer = false; @@ -301,7 +321,9 @@ std::set CodeInstructionCollector::Collect(const std::ve afterTransfer = info.FlowKind != Codegen::ControlFlowKind::Sequential && info.FlowKind != Codegen::ControlFlowKind::ConditionalBranch; position = next; } + scannedBytes += header.FileSize; } + report(6000); const auto decodesInSegment = [&](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) continue; @@ -333,7 +355,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i]); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }); for (const auto address : graph->ReachableVaddrs()) { if (!decodedInstructions.insert(address).second) continue; instructions.insert(address); @@ -344,8 +366,12 @@ std::set CodeInstructionCollector::Collect(const std::ve } } } while (roots.size() != previousRoots); + report(8500); + std::uint64_t checked = 0; std::uint64_t previousEnd = 0; for (const auto address : instructions) { + if (progress && checked % 4096 == 0) report(8500 + 1499 * checked / instructions.size()); + ++checked; if (address < previousEnd) { std::uint64_t spanEnd = previousEnd; try { @@ -394,6 +420,7 @@ std::set CodeInstructionCollector::Collect(const std::ve break; } } + report(10000); return instructions; } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index f83eb1eead..128ededb0c 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -5,7 +5,7 @@ #include #include #include -#include + #include #include #include @@ -71,9 +71,7 @@ static constexpr std::array SDK_MMAP_EXEC_INLINE{ }; template -static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, - const std::array& pattern, std::size_t beforeSyscall, - const std::vector& matches, FileByteOffset codeOffset) { +static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const std::array& pattern, std::size_t beforeSyscall, const std::vector& matches, FileByteOffset codeOffset) { if (syscallOffset < beforeSyscall || syscallOffset > limit) return false; const auto start = syscallOffset - beforeSyscall; if (pattern.size() > limit - start || !std::equal(pattern.begin(), pattern.end(), code.begin() + static_cast(start))) return false; @@ -82,17 +80,11 @@ static bool MatchesSdkInlineStub(const std::vector& code, std::siz return instruction != matches.end() && instruction->Offset == codeOffset + start; } -static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, - const std::vector& matches, FileByteOffset codeOffset) { - return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) - || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) - || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); +static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, const std::vector& matches, FileByteOffset codeOffset) { + return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); } -static ForbiddenInstruction ClassifyForbiddenInstruction( - const std::vector& codeSection, - const std::size_t opcodeOffset, - const std::size_t limit) { +static ForbiddenInstruction ClassifyForbiddenInstruction(const std::vector& codeSection, const std::size_t opcodeOffset, const std::size_t limit) { if (opcodeOffset + 1 >= limit) return ForbiddenInstruction::None; @@ -133,11 +125,7 @@ static std::optional ConstantRaxLoad(const std::uint8_t* bytes, c return std::nullopt; } -static std::optional ConstantSyscallNumber( - const std::vector& codeSection, - const FileByteOffset codeSectionOffset, - const std::vector& matches, - const std::size_t syscallIndex) { +static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const std::vector& matches, const std::size_t syscallIndex) { for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step <= syscallIndex; ++step) { const auto& match = matches[syscallIndex - step]; const std::size_t start = static_cast(match.Offset - codeSectionOffset); @@ -164,28 +152,26 @@ static std::optional ConstantSyscallNumber( class SyscallScanner : public ISyscallScanner { public: - explicit SyscallScanner(const bool trapSdkStubs) : _trapSdkStubs(trapSdkStubs) {} + SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} - std::vector ScanCodeSectionForSyscalls( - const std::vector& codeSection, - FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize) override; + std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input) override; private: - bool _trapSdkStubs; + Domain::IProgress& progress; + bool trapSdkStubs; }; -std::vector SyscallScanner::ScanCodeSectionForSyscalls( - const std::vector& codeSection, - const FileByteOffset codeSectionOffset, - const FileByteOffset codeSectionSize) { - const std::size_t limit = std::min(codeSection.size(), static_cast(codeSectionSize)); +std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize, std::string_view input) { + if (codeSectionSize > codeSection.size()) throw RelinkerException("Code section exceeds the input buffer", codeSectionOffset); + const auto limit = static_cast(codeSectionSize); - const auto scanner = Codegen::MakeInstructionScanner(); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, std::string(input), completed, total}); }); const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); + progress.Report({Domain::ProgressStage::Syscalls, std::string(input), 0, matches.size()}); std::vector trapped; for (std::size_t index = 0; index < matches.size(); ++index) { + if (index != 0 && index % 4096 == 0) progress.Report({Domain::ProgressStage::Syscalls, std::string(input), index, matches.size()}); const auto& match = matches[index]; const std::size_t start = static_cast(match.Offset - codeSectionOffset); const auto opcodeOffset = Codegen::DecodedInstruction{codeSection.data() + start, match.Length}.OpcodeOffset(); @@ -197,10 +183,12 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; - if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && + if (trapSdkStubs && kind == ForbiddenInstruction::Invocation && (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, matches, codeSectionOffset))) { trapped.push_back(opcode); - std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; + std::ostringstream message; + message << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << '\n'; + progress.Message(message.str()); continue; } @@ -213,20 +201,17 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( throw RelinkerException(msg.str(), match.Offset); } + progress.Report({Domain::ProgressStage::Syscalls, std::string(input), matches.size(), matches.size()}); return trapped; } -std::unique_ptr MakeSyscallScanner(const bool trapSdkStubs) { - return std::make_unique(trapSdkStubs); +std::unique_ptr MakeSyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) { + return std::make_unique(progress, trapSdkStubs); } class NullSyscallScanner : public ISyscallScanner { public: - std::vector ScanCodeSectionForSyscalls( - const std::vector&, - FileByteOffset, - FileByteOffset - ) override { return {}; } + std::vector ScanCodeSectionForSyscalls(const std::vector&, FileByteOffset, FileByteOffset, std::string_view) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 18842fd8c7..bf4c1f76d9 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -29,16 +29,7 @@ private: namespace { -void buildGraph( - std::span text, - VirtualAddress textVaddr, - VirtualAddress entryVaddr, - const std::vector& extraEntries, - const IRelativeRelocationIndex& relativeRelocations, - const std::vector* data, - bool followCodeAddresses, - std::unordered_set& reachable -) { +void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress) { const Codegen::X64InstructionDecoder decoder; std::unordered_set visited; std::queue worklist; @@ -90,6 +81,7 @@ void buildGraph( info = decoder.DecodeInstruction(text.data() + bufOff, available); } reachable.insert(va); + if (progress && reachable.size() % 4096 == 0) progress(reachable.size()); visited.erase(va); VirtualAddress nextVaddr = va + static_cast(info.Length); @@ -162,16 +154,8 @@ void buildGraph( } -std::unique_ptr BuildControlFlowGraph( - std::span text, - VirtualAddress textVaddr, - VirtualAddress entryVaddr, - const std::vector& extraEntries, - const IRelativeRelocationIndex& relativeRelocations, - bool followCodeAddresses, - std::unordered_set& reachable -) { - buildGraph(text, textVaddr, entryVaddr, extraEntries, relativeRelocations, nullptr, followCodeAddresses, reachable); +std::unique_ptr BuildControlFlowGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress) { + buildGraph(text, textVaddr, entryVaddr, extraEntries, relativeRelocations, nullptr, followCodeAddresses, reachable, progress); return std::make_unique(&reachable); } @@ -208,7 +192,7 @@ std::unique_ptr BuildAddressTakenControlFlowGraph( const std::vector& data ) { std::unordered_set reachable; - buildGraph(std::span(text.data(), text.size()), textVaddr, entryVaddr, extraEntries, relativeRelocations, &data, false, reachable); + buildGraph(std::span(text.data(), text.size()), textVaddr, entryVaddr, extraEntries, relativeRelocations, &data, false, reachable, {}); return std::make_unique(std::move(reachable)); } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index 3eede671eb..2fb2cc1158 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -188,7 +188,10 @@ private: }; class StrictNidFilter : public IUnusedNidFilter { + Domain::IProgress& progress; + 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 { const StrictImage image(elfBytes); const auto input = image.Build(nidRefs, textSection, textVAddr); @@ -197,16 +200,16 @@ public: for (const auto& reference : nidRefs) { if (analysis.ImportSlots.contains(reference.RelocationAddress)) result.push_back(reference); } - std::cout << "Strict reachability: " << analysis.Instructions.size() << " instructions; conservative indirect transfers=" << analysis.IndirectTransfers << "\n"; - std::cout << "Function graph: " << analysis.LiveRegions << "/" << analysis.TotalRegions << " live regions; address-taken roots=" << analysis.AddressTakenRoots << "; unwind functions=" << input.Functions.size() << "\n"; + { std::ostringstream message; message << "Strict reachability: " << analysis.Instructions.size() << " instructions; conservative indirect transfers=" << analysis.IndirectTransfers << "\n"; progress.Message(message.str()); } + { std::ostringstream message; message << "Function graph: " << analysis.LiveRegions << "/" << analysis.TotalRegions << " live regions; address-taken roots=" << analysis.AddressTakenRoots << "; unwind functions=" << input.Functions.size() << "\n"; progress.Message(message.str()); } return result; } }; } -std::shared_ptr MakeStrictUnusedNidFilter() { - return std::make_shared(); +std::shared_ptr MakeStrictUnusedNidFilter(Domain::IProgress& progress) { + return std::make_shared(progress); } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 71fd7342ef..21229806dd 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -163,7 +163,8 @@ private: if (info.HasRipRelativeDisp) { std::int32_t displacement; std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement)); - const auto target = next + static_cast(static_cast(displacement)); + auto target = next + static_cast(static_cast(displacement)); + if (info.AddressSize32) target = static_cast(target); if (input.ImportSlots.contains(target)) region.Imports.insert(target); if (!info.IsTwoByteOpcode && info.Opcode == 0x8D) { addAddressTaken(target); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 52cf793e56..c07cb75358 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -148,7 +148,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (tag == 1) needed.push_back(value); else if (tag == 0x61000045) moduleImports.push_back(value); else if (tag == 0x6100000d || tag == 0x61000043) moduleExports.push_back(value); - else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { + else if (tag < 0x60000000 || tag == 0x6ffffef5 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } } @@ -194,7 +194,35 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector std::uint64_t symSize = 0; if (tags.contains(0x6100003f)) symSize = tags.at(0x6100003f); else if (tags.contains(4)) symSize = static_cast(Io::ReadU32(bytes, translate(tags.at(4), 8) + 4)) * 24; - else fail("Missing dynamic symbol count"); + else if (tags.contains(0x6ffffef5)) { + const auto address = tags.at(0x6ffffef5); + const auto header = translate(address, 16); + const auto bucketCount = Io::ReadU32(bytes, header); + const auto firstSymbol = Io::ReadU32(bytes, header + 4); + const auto bloomCount = Io::ReadU32(bytes, header + 8); + if (bucketCount == 0 || bloomCount == 0 || firstSymbol == 0) fail("Invalid GNU hash header"); + const std::uint64_t bucketOffset = 16 + static_cast(bloomCount) * 8; + const auto chainOffset = bucketOffset + static_cast(bucketCount) * 4; + const auto start = translate(address, chainOffset); + std::uint64_t lastBucket = 0; + for (std::uint64_t index = 0; index < bucketCount; ++index) { + const auto bucket = Io::ReadU32(bytes, start + bucketOffset + index * 4); + if (bucket != 0 && bucket < firstSymbol) fail("Invalid GNU hash bucket"); + lastBucket = std::max(lastBucket, static_cast(bucket)); + } + std::uint64_t symbolCount = firstSymbol; + if (lastBucket != 0) { + symbolCount = lastBucket; + for (;;) { + const auto offset = chainOffset + (symbolCount - firstSymbol) * 4; + if (offset > std::numeric_limits::max() - address) fail("Invalid GNU hash chain"); + const auto chain = Io::ReadU32(bytes, translate(address + offset, 4)); + ++symbolCount; + if (chain & 1) break; + } + } + symSize = symbolCount * 24; + } else fail("Missing dynamic symbol count"); if (symSize == 0 || symSize % 24 != 0 || symSize / 24 > std::numeric_limits::max()) fail("Invalid dynamic symbol count"); const auto symOffset = table(6, 0x61000039, symSize); image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 600209a63d..8a7d139f7c 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -2,14 +2,10 @@ #include #include #include -#include #include #include #include -#include #include -#include -#include #include #include #include @@ -59,59 +55,20 @@ std::string ModuleStem(std::string name, const bool windows) { } -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& sceModulePath, const std::vector& moduleDirectories) const { - const auto root = std::filesystem::absolute(sceModulePath).lexically_normal(); - if (!std::filesystem::exists(root)) throw Domain::RelinkerException("Guest module parent directory does not exist: " + Io::Utf8Path(root)); - if (!std::filesystem::is_directory(root)) throw Domain::RelinkerException("Guest module parent path is not a directory: " + Io::Utf8Path(root)); - const auto missingModuleMessage = std::string(GuestModulePattern) + " was not found in: " + Io::Utf8Path(root) + ". Use --skip-sce-module to disable guest module processing."; - const auto singular = root / "sce_module"; - const auto plural = root / "sce_modules"; - const auto prx = root / "prx"; - const bool hasSingular = std::filesystem::exists(singular); - const bool hasPlural = std::filesystem::exists(plural); - const bool hasPrx = std::filesystem::exists(prx); - if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist in the guest module parent directory"); - if (!hasSingular && !hasPlural && !hasPrx) throw Domain::RelinkerException(missingModuleMessage); - std::vector directories; - if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); - if (hasPrx) directories.push_back(prx); - std::vector runtimeDirectories; - for (const auto& name : moduleDirectories) { - const auto relative = Io::NativePath(name).lexically_normal(); - if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") - throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name); - auto directory = (root / relative).lexically_normal(); - if (!directory.has_filename()) directory = directory.parent_path(); - if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); - if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); - directories.push_back(directory); - runtimeDirectories.push_back(directory); - } +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool macos, const bool toIntel, const bool toRosetta, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules, const std::filesystem::path& root, const std::vector& runtimeDirectories, std::vector modules, Domain::IProgress& progress) const { std::vector paths; - std::set unmatchedExclusions = excludedModules; - const auto isElf = [](const std::filesystem::path& path) { - std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); - if (stream.gcount() != 4) return false; - const auto byte = [&](const std::size_t index) { return static_cast(magic[index]); }; - const bool self = (byte(0) == 0x4f && byte(1) == 0x15 && byte(2) == 0x3d && byte(3) == 0x1d) || (byte(0) == 0x54 && byte(1) == 0x14 && byte(2) == 0xf5 && byte(3) == 0xee); - return byte(0) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; - }; - for (const auto& directory : directories) { - if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); - for (const auto& entry : std::filesystem::directory_iterator(directory)) { - if (Io::Utf8Path(entry.path().filename()).ends_with(GuestModuleSuffix)) continue; - if (excludedModules.contains(Io::Utf8Path(entry.path().filename()))) { - unmatchedExclusions.erase(Io::Utf8Path(entry.path().filename())); - continue; - } - if (!entry.is_regular_file()) continue; - if (std::filesystem::equivalent(entry.path(), inputPath)) continue; - if (isElf(entry.path())) paths.push_back(entry.path()); - } + std::map discovered; + progress.Report({Domain::ProgressStage::Modules, {}, 0, modules.size()}); + for (auto& module : modules) { + const auto path = module.Path.lexically_normal(); + if (path == inputPath.lexically_normal()) throw Domain::RelinkerException("Module path collides with the input executable: " + Io::Utf8Path(path)); + if (path == outputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(path)); + const auto relative = path.lexically_relative(root); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") throw Domain::RelinkerException("Module is outside module root: " + Io::Utf8Path(path)); + if (excludedModules.contains(Io::Utf8Path(path.filename()))) throw Domain::RelinkerException("Excluded module supplied to converter: " + Io::Utf8Path(path)); + if (discovered.contains(path)) throw Domain::RelinkerException("Duplicate module path: " + Io::Utf8Path(path)); + paths.push_back(path); + discovered.emplace(path, GuestImageReader().Read(path, std::move(module.Bytes), windows)); } const auto neededNames = ReadNeededNames(dynamic); std::set missingNeeded; @@ -124,24 +81,15 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (alias != paths.end()) neededAliases.emplace(name, *alias); else missingNeeded.insert(name); } - Io::FileReader reader; - std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; - for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); - if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; - if (std::filesystem::exists(outputPath)) { - for (const auto& path : paths) - if (std::filesystem::equivalent(path, outputPath)) - throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(outputPath)); - } std::unordered_map> exports; std::map> sharedExports; std::set outputNames; @@ -342,23 +290,21 @@ 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"); - const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); + const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { auto& image = images[index]; - std::ostringstream moduleSize; - moduleSize << std::fixed << std::setprecision(2) << static_cast(image.Bytes.size()) / 1000000.0; - std::cout << "Processing " << image.SourcePath.lexically_relative(root).generic_string() << " (" << moduleSize.str() << "MB)" << std::flush; + progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); if (toIntel) { - auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), image.Headers); + auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Lowering, Io::Utf8Path(image.SourcePath), completed, total}); })->Convert(std::move(image.Bytes), image.Headers); image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } for (const auto& header : codeHeaders) { const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize)) { + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; @@ -368,9 +314,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto relativeDirectory = appDirectory(image); const auto destination = outputDirectory / relativeDirectory; const auto target = destination / image.OutputName; - if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); - for (const auto& source : paths) if (std::filesystem::exists(target) && std::filesystem::equivalent(source, target)) throw Domain::RelinkerException("Guest output would overwrite an input module: " + Io::Utf8Path(target)); - if (std::filesystem::exists(target) && std::filesystem::equivalent(inputPath, target)) throw Domain::RelinkerException("Guest output would overwrite the input executable"); + if (target.lexically_normal() == outputPath.lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); Domain::GuestRuntime runtime; runtime.UsePlatformTlsResolver = image.UsePlatformTlsResolver; runtime.Path = relativeDirectory + "/" + image.OutputName; @@ -413,7 +357,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } dynamic.GuestModules.push_back(std::move(runtime)); artifacts.push_back({target, std::move(output), std::move(imports), image.SourcePath}); - std::cout << " [DONE]" << std::endl; + progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); } return artifacts; } diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index b639608662..65441ef8cb 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -84,7 +84,7 @@ 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((STB_GLOBAL << 4) | STT_FUNC); + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC); }; static constexpr std::size_t kRelaEntSize = 24; @@ -94,6 +94,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const auto& ref : nidReferences) { if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); + if (ref.Function) result.FunctionImports.insert(ref.RelocationAddress); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 47f2cd0583..a214a56f8d 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -88,6 +88,10 @@ ElfHeader ElfReader::ReadHeader() const { throw RelinkerException("Expected little-endian ELF64 version 1"); if (_readU16At(0x12) != 62) throw RelinkerException("Expected an x86-64 ELF"); + if (_readU32At(0x14) != 1) + throw RelinkerException("Unsupported ELF object version: expected 1", 0x14); + if (_readU16At(0x34) != 64) + throw RelinkerException("Invalid ELF header size: expected 64 bytes", 0x34); ElfHeader header{}; header.Machine = _readU16At(0x12); diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp new file mode 100644 index 0000000000..39582c4960 --- /dev/null +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {} + +ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const { + 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"); + if (options.ToRosetta && (!options.ToIntel || options.Target == TargetSystem::Windows)) throw Domain::RelinkerException("Rosetta conversion requires Intel lowering and a non-Windows target"); + if (options.UnusedFilterLevel > 2) throw Domain::RelinkerException("Unused NID filter level must be 0, 1 or 2"); + if (input.InputPath.empty() || input.OutputPath.empty()) throw Domain::RelinkerException("Input and output names are required"); + 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"); + 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}); + auto sourceBytes = std::move(input.Executable); + std::vector trampolines; + if (options.ToIntel) { + const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + auto converted = Codegen::MakeAmd64OnlyConverter(options.ToRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Lowering, {}, completed, total}); })->Convert(std::move(sourceBytes), segments); + sourceBytes = std::move(converted.Bytes); + trampolines = std::move(converted.Trampolines); + std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; + for (const auto& report : converted.Reports) { + if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) + { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; inputProgress.Message(message.str()); } + else if (counted(report.InstructionName)) + ++stubsByName[report.InstructionName]; + else + { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; inputProgress.Message(message.str()); } + } + for (const auto& [name, count] : stubsByName) + { std::ostringstream message; message << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; inputProgress.Message(message.str()); } + { std::ostringstream message; message << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; inputProgress.Message(message.str()); } + } + + auto elfReader = std::make_shared(sourceBytes); + const std::shared_ptr syscallScanner = options.SkipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(inputProgress, options.TrapSdkSyscallStubs); + + const auto pipeline = std::make_shared( + elfReader, + syscallScanner, + Relinker::MakeCallSiteResolver(), + std::make_shared(), + std::make_shared(), + options.UnusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter(inputProgress) : Relinker::MakeUnusedNidFilter(), + options.UnusedFilterLevel, inputProgress + ); + + { std::ostringstream message; message << "System: " << (windows ? "Windows" : macos ? "macOS" : "Linux") << "; unused-filter=" << options.UnusedFilterLevel << "\n"; inputProgress.Message(message.str()); } + for (const auto& name : options.ExcludedModules) { std::ostringstream message; message << "Guest module excluded: " << name << '\n'; inputProgress.Message(message.str()); } + auto result = pipeline->Relink(sourceBytes); + for (const auto& patch : result.Patches) { + if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) + throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); + for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + } + + std::vector guestArtifacts; + if (!input.Modules.empty()) { + guestArtifacts = Relinker::GuestModuleBuilder().Build(input.InputPath, input.OutputPath, result.DynamicSection, windows, macos, options.ToIntel, options.ToRosetta, *syscallScanner, options.LazyBinding, options.RunPath, options.ExcludedModules, input.ModuleRoot, input.RuntimeDirectories, std::move(input.Modules), inputProgress); + } + + auto byteWriter = std::make_shared(); + + std::shared_ptr patcher; + if (macos) { + patcher = std::make_shared(); + } else if (windows) { + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); + } else { + patcher = std::make_shared( + std::make_shared(), + std::make_shared( + std::make_shared(), + byteWriter + ), + std::make_shared(byteWriter), + byteWriter + ); + } + + if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); + std::vector executableBytes; + 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); + throw; + } + + if (!windows) inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 1, 1}); + std::string diagnostics; + 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)}; +} + +} diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 64aa5b865d..bd98088047 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -6,7 +6,7 @@ #include #include #include -#include + #include #include #include @@ -16,19 +16,21 @@ namespace Relinker { using namespace Elfpatcher; -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) - : _elfReader(std::move(elfReader)) - , _syscallScanner(std::move(syscallScanner)) - , _callSiteResolver(std::move(callSiteResolver)) - , _validationPolicy(std::move(validationPolicy)) - , _dynamicSectionBuilder(std::move(dynamicSectionBuilder)) - , _unusedNidFilter(std::move(unusedNidFilter)) +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)) + , callSiteResolver(std::move(callSiteResolver)) + , validationPolicy(std::move(validationPolicy)) + , dynamicSectionBuilder(std::move(dynamicSectionBuilder)) + , unusedNidFilter(std::move(unusedNidFilter)) , unusedFilterLevel(unusedFilterLevel) + , progress(progress) { + if (!this->elfReader || !this->syscallScanner || !this->callSiteResolver || !this->validationPolicy || !this->dynamicSectionBuilder || !this->unusedNidFilter) throw RelinkerException("Missing relinker pipeline dependency"); if (unusedFilterLevel > 2) throw RelinkerException("Unused NID filter level must be 0, 1 or 2"); } -std::string RelinkerPipeline::_relocationTypeName(std::uint32_t type) { +std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { switch (type) { case 1: return "R_X86_64_64"; case 6: return "R_X86_64_GLOB_DAT"; @@ -43,7 +45,7 @@ std::string RelinkerPipeline::_relocationTypeName(std::uint32_t type) { } RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf) { - auto programHeaders = _elfReader->ReadProgramHeaders(); + auto programHeaders = elfReader->ReadProgramHeaders(); std::vector textSection; VirtualAddress textVAddr = 0; @@ -53,7 +55,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (const auto& ph : programHeaders) { if (ph.Type == PT_LOAD && (ph.Flags & PF_X) != 0) { - auto segment = _elfReader->ReadSegment(ph); + auto segment = elfReader->ReadSegment(ph); if (!segment.empty()) { if (textSection.empty()) { textSection = segment; @@ -83,7 +85,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; hasDynamicSegment = true; - dynTags = _elfReader->ReadDynamicTags(ph); + dynTags = elfReader->ReadDynamicTags(ph); break; } @@ -121,7 +123,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf throw RelinkerException(std::string("Adding PT_SCE_DYNLIBDATA base to ") + name + " overflows", value); return sceTableBase + value; } - return _elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); + return elfReader->TranslateVirtualAddress(getTagValue(sysvTag)); }; auto readAsSize = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> ByteCount { @@ -175,9 +177,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector nidRefs; std::vector neededLibraries; - auto policy = std::dynamic_pointer_cast(_validationPolicy); + auto policy = std::dynamic_pointer_cast(validationPolicy); - const std::vector& raw = _elfReader->GetRawBytes(); + const std::vector& raw = elfReader->GetRawBytes(); if (dynStrTabOffset > raw.size() || dynStrTabSize > raw.size() - dynStrTabOffset) throw RelinkerException("Dynamic string table is out of bounds", dynStrTabOffset); @@ -251,7 +253,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 4) + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) throw RelinkerException("Symbol table entry out of bounds", symOff); std::uint32_t nameOff = 0; @@ -262,7 +264,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), [](const std::uint8_t byte) { return byte != 0; }))) throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); + const bool weak = (raw[symOff + 4] >> 4) == STB_WEAK; + const bool function = (raw[symOff + 4] & 15) == STT_FUNC; + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, weak, function}); } }; @@ -270,7 +274,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf extractRela(dynJmpRelOffset, dynJmpRelSize); for (const auto& ref : nidRefs) - _validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); + validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); @@ -312,7 +316,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector syscallPatches; for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { - for (const auto offset : _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + for (const auto offset : syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { if (offset > segment.size() || segment.size() - offset < 2u) throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); segment[offset] = 0x0F; @@ -321,18 +325,19 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } } - _validationPolicy->ValidateSyscallAbsence(); + validationPolicy->ValidateSyscallAbsence(); + progress.Report({Domain::ProgressStage::Imports, {}, 0, 1}); const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; - std::cout << "NID input: " << originalNidCount << " references\n"; + { std::ostringstream message; message << "NID input: " << originalNidCount << " references\n"; progress.Message(message.str()); } if (unusedFilterLevel == 2) { if (executableSegments.size() > 1) throw RelinkerException("Strict NID filtering does not support multiple executable segments"); - nidRefs = _unusedNidFilter->Filter(nidRefs, raw, textSection, textVAddr); + nidRefs = unusedNidFilter->Filter(nidRefs, raw, textSection, textVAddr); if (nidRefs.size() > originalNidCount) throw RelinkerException("Strict NID filter increased the reference count"); - std::cout << "Strict filtering total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; + { std::ostringstream message; message << "Strict filtering total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; progress.Message(message.str()); } } else if (unusedFilterLevel == 1) { std::vector pltRefs; std::vector nonPltRefs; @@ -343,15 +348,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf nonPltRefs.push_back(ref); } - std::cout << "PLT preservation: " << pltRefs.size() << " -> " << pltRefs.size() << "; filtered=0\n"; + { std::ostringstream message; message << "PLT preservation: " << pltRefs.size() << " -> " << pltRefs.size() << "; filtered=0\n"; progress.Message(message.str()); } const std::size_t nonPltCount = nonPltRefs.size(); if (executableSegments.size() > 1) { - std::cout << "CFG/GOT filtering skipped: multiple executable segments are not modeled; " - << nonPltCount << " -> " << nonPltCount << "; filtered=0\n"; + { 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); if (nonPltRefs.size() > nonPltCount) throw RelinkerException("Unused NID filter increased the reference count"); - std::cout << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; + { std::ostringstream message; message << "CFG/GOT filtering: " << nonPltCount << " -> " << nonPltRefs.size() << "; filtered=" << nonPltCount - nonPltRefs.size() << "\n"; progress.Message(message.str()); } } nidRefs.clear(); @@ -359,22 +364,22 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (auto& ref : pltRefs) nidRefs.push_back(std::move(ref)); for (auto& ref : nonPltRefs) nidRefs.push_back(std::move(ref)); } else { - std::cout << "Unused NID filtering: disabled; filtered=0\n"; + { std::ostringstream message; message << "Unused NID filtering: disabled; filtered=0\n"; progress.Message(message.str()); } } - std::cout << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; + { std::ostringstream message; message << "NID total: " << originalNidCount << " -> " << nidRefs.size() << "; filtered=" << originalNidCount - nidRefs.size() << "\n"; progress.Message(message.str()); } auto dynamicRefs = nidRefs; std::vector patches; auto pltCount = static_cast(dynJmpRelSize / relaEntSize); if (unusedFilterLevel == 2) { - auto compacted = UnusedNidFilter::CompactPlt(originalNidRefs, nidRefs, textSection, textVAddr, _elfReader->TranslateVirtualAddress(textVAddr), dynJmpRelOffset); + auto compacted = UnusedNidFilter::CompactPlt(originalNidRefs, nidRefs, textSection, textVAddr, elfReader->TranslateVirtualAddress(textVAddr), dynJmpRelOffset); dynamicRefs = std::move(compacted.References); patches = std::move(compacted.Patches); - std::cout << "PLT compaction: " << pltCount << " -> " << compacted.SlotCount << "\n"; + { std::ostringstream message; message << "PLT compaction: " << pltCount << " -> " << compacted.SlotCount << "\n"; progress.Message(message.str()); } pltCount = compacted.SlotCount; } - auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); + auto dynSection = dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); for (const auto& ref : dynamicRefs) { if (!ref.Library.empty()) continue; if (auto declared = Domain::DeclaredImportModule(ref.Nid, importModules); !declared.empty()) dynSection.UnmatchedImportModules.insert(std::move(declared)); @@ -434,7 +439,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf CallRegistryEntry entry; entry.Nid = ref.Nid; entry.Library = ref.Library; - entry.RelocationTypeString = _relocationTypeName(ref.RelocationTypeValue); + entry.RelocationTypeString = relocationTypeName(ref.RelocationTypeValue); entry.RelocationOffset = ref.RelocationTableOffset; entry.TargetSection = ".got"; entry.TargetOffset = ref.RelocationAddress; @@ -445,13 +450,14 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { static_cast(segmentFileOffset); for (auto& entry : entries) { - auto segmentSites = _callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); + auto segmentSites = callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); entry.CallSitesResolved = !entry.CallSites.empty(); } } patches.insert(patches.end(), std::make_move_iterator(syscallPatches.begin()), std::make_move_iterator(syscallPatches.end())); + progress.Report({Domain::ProgressStage::Imports, {}, 1, 1}); return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr, std::move(patches)}; } diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp new file mode 100644 index 0000000000..a2ff847e12 --- /dev/null +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -0,0 +1,200 @@ +#include "TestProgress.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void reject(TAction action) { + try { action(); } catch (const Domain::RelinkerException&) { return; } + throw std::runtime_error("Invalid conversion was accepted"); +} + +std::vector fixture() { + std::vector bytes(0x3000); + const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::copy(magic.begin(), magic.end(), bytes.begin()); + Io::WriteU16(bytes, 16, 3); + Io::WriteU16(bytes, 18, 62); + Io::WriteU32(bytes, 20, 1); + Io::WriteU64(bytes, 24, 0x1000); + Io::WriteU64(bytes, 32, 64); + Io::WriteU16(bytes, 52, 64); + Io::WriteU16(bytes, 54, 56); + Io::WriteU16(bytes, 56, 8); + Io::WriteU16(bytes, 58, 64); + const auto header = [&](std::size_t index, unsigned type, unsigned flags, unsigned offset, unsigned size) { + const auto at = 64 + index * 56; + Io::WriteU32(bytes, at, type); + Io::WriteU32(bytes, at + 4, flags); + Io::WriteU64(bytes, at + 8, offset); + Io::WriteU64(bytes, at + 16, offset); + Io::WriteU64(bytes, at + 24, offset); + Io::WriteU64(bytes, at + 32, size); + Io::WriteU64(bytes, at + 40, size); + Io::WriteU64(bytes, at + 48, 8); + }; + header(0, 1, 5, 0x1000, 8); + header(1, 1, 6, 0x2000, 0x1000); + for (std::size_t index = 3; index < 8; ++index) header(index, 0x6fffff01, 0, 0, 0); + bytes[0x1000] = 0xc3; + const std::array, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}}; + header(2, 2, 6, 0x2000, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(bytes, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU32(bytes, 0x2680, 1); + Io::WriteU32(bytes, 0x2684, 1); + return bytes; +} + +Relinker::ConversionInput input() { + Relinker::ConversionInput result; + result.Executable = fixture(); + result.InputPath = "virtual/title/custom-name.bin"; + result.OutputPath = "virtual/output/title"; + result.ModuleRoot = "virtual/title"; + return result; +} + +void convert() { + TestProgress progress; + const Relinker::Conversion converter(progress); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::Linux}) { + Relinker::ConversionOptions options; + options.Target = target; + options.ToIntel = true; + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto output = converter.Convert(std::move(source), options); + require(output.Executable.size() > 64, "No executable returned"); + require(output.Executable.front() == (target == Relinker::TargetSystem::Windows ? 'M' : 0x7f), "Wrong target format"); + require(output.Modules.size() == 1, "Module was not converted"); + require(output.Modules.front().Path.generic_string() == "virtual/output/app0/sce_module/plugin.prx.guest.prx", "Wrong module layout"); + require(output.Modules.front().Bytes.front() == output.Executable.front(), "Wrong module format"); + require(progress.Events.back().Stage == Domain::ProgressStage::Executable && progress.Events.back().Completed == 1, "Conversion did not finish"); + for (const auto& event : progress.Events) require(event.Completed <= event.Total, "Invalid counters"); + if (target == Relinker::TargetSystem::Windows) { + std::uint64_t previous = 0; + bool intermediate = false; + for (const auto& event : progress.Events) { + if (event.Stage != Domain::ProgressStage::Image) continue; + require(event.Input == "virtual/title/custom-name.bin", "Lost image input identity"); + require(event.Total == 10000 && event.Completed >= previous, "Image progress moved backwards"); + intermediate |= event.Completed > 0 && event.Completed < event.Total; + previous = event.Completed; + } + require(intermediate && previous == 10000, "Missing image progress"); + } + progress.Events.clear(); + } + auto source = input(); + source.Modules.push_back({"virtual/escaped.prx", fixture()}); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + source.Modules.push_back(source.Modules.front()); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Icon = std::vector{}; + Relinker::ConversionOptions windows; + windows.Target = Relinker::TargetSystem::Windows; + reject([&] { converter.Convert(source, windows); }); + source = input(); + source.Executable = {}; + reject([&] { converter.Convert(source, {}); }); + require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); +} + +void scanning() { + TestProgress progress; + const auto scanner = Relinker::MakeSyscallScanner(progress); + std::vector code(150000, 0x90); + require(scanner->ScanCodeSectionForSyscalls(code, 0, code.size(), "sample.prx").empty(), "Unexpected syscall trap"); + require(std::any_of(progress.Events.begin(), progress.Events.end(), [](const auto& event) { return event.Completed > 0 && event.Completed < event.Total; }), "Only start and finish were reported"); + for (const auto& event : progress.Events) require(event.Input == "sample.prx", "Lost scanner input identity"); + progress.Events.clear(); + Domain::ProgramHeader header{}; + header.Type = 1; + header.Flags = 1; + header.FileSize = code.size(); + header.MemorySize = code.size(); + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Lowering, "sample.prx", completed, total}); }); + require(converter->Convert(code, {header}).Bytes == code, "Progress changed instruction conversion"); + std::uint64_t previous = 0; + for (const auto& event : progress.Events) { + require(event.Completed >= previous && event.Completed <= event.Total, "Instruction conversion progress moved backwards"); + previous = event.Completed; + } + require(previous == progress.Events.back().Total, "Instruction conversion progress did not finish"); + reject([&] { scanner->ScanCodeSectionForSyscalls(code, 0, (std::uint64_t{1} << 32) + code.size()); }); + auto image = code; + image.resize(code.size() + 64, 0x90); + image.back() = 0xc3; + Io::WriteU64(image, 24, 64); + header.Offset = 64; + header.MappedAddress = 64; + previous = 0; + std::size_t updates = 0; + const auto instructions = Relinker::CodeInstructionCollector().Collect(image, {header}, [&](auto completed, auto total) { + require(completed >= previous && completed <= total, "Image analysis progress moved backwards"); + previous = completed; + ++updates; + }); + require(instructions.size() == code.size(), "Progress changed image code analysis"); + require(updates > 10 && previous == 10000, "Missing intermediate image analysis progress"); + code[70000] = 0x0f; + code[70001] = 0x05; + progress.Events.clear(); + reject([&] { scanner->ScanCodeSectionForSyscalls(code, 0, code.size()); }); + require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); +} + +class CancellingProgress final : public Domain::IProgress { +public: + Domain::ProgressStage Stage = Domain::ProgressStage::Decode; + void Report(const Domain::ProgressEvent& event) override { + if (event.Stage == Stage && event.Completed > 0) throw std::runtime_error("cancelled"); + } + void Message(std::string_view) override {} +}; + +void cancellation() { + for (const auto stage : {Domain::ProgressStage::Decode, Domain::ProgressStage::Image}) { + CancellingProgress progress; + progress.Stage = stage; + Relinker::ConversionOptions options; + options.Target = Relinker::TargetSystem::Windows; + try { Relinker::Conversion(progress).Convert(input(), options); } + catch (const std::runtime_error& error) { + require(std::string(error.what()) == "cancelled", "Callback exception was replaced"); + continue; + } + throw std::runtime_error("Callback exception was swallowed"); + } +} + +} + +int main() { + try { + convert(); + scanning(); + cancellation(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp index 264cb6057c..1ede68a821 100644 --- a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -1,3 +1,4 @@ +#include "TestProgress.hpp" #include #include #include @@ -30,7 +31,7 @@ void Require(bool value, const std::string& message) { bool Rejected(const Bytes& code, bool trap, std::size_t size) { try { - Relinker::MakeSyscallScanner(trap)->ScanCodeSectionForSyscalls(code, 0, size); + Relinker::MakeSyscallScanner(testProgress, trap)->ScanCodeSectionForSyscalls(code, 0, size); return false; } catch (const std::runtime_error&) { return true; @@ -44,7 +45,7 @@ void CheckFixtures() { for (const auto padding : {std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{64}}) { Bytes code(padding, 0x90); code.insert(code.end(), fixture.code.begin(), fixture.code.end()); - const auto trapped = Relinker::MakeSyscallScanner(true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); + const auto trapped = Relinker::MakeSyscallScanner(testProgress, true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); Require(trapped == std::vector{padding + fixture.syscall}, "SDK inline syscall returned the wrong patch offset"); } for (std::size_t length = fixture.syscall + 2; length < fixture.code.size(); ++length) diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index 4d3f257559..bc31840e61 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -1,3 +1,4 @@ +#include "TestProgress.hpp" #include #include #include @@ -98,6 +99,22 @@ void fallthroughAfterZeroPadding() { require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Zero padding after a region's last jump dropped its fall-through edge"); } +void addressSizeOverrideImport() { + auto input = fixture(); + input.Text.assign(32, 0); + input.TextVaddr = 0x100001000ull; + input.Entries = {input.TextVaddr}; + input.ImportSlots = {0x2000, 0x100002000ull}; + emit(input, 0, {0x67, 0xFF, 0x15, 0xF9, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "EIP-relative import was not resolved with 32-bit address wrapping"); + input.Text.assign(32, 0); + emit(input, 0, {0x67, 0x48, 0x8B, 0x05, 0xF8, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "REX.W EIP-relative load did not resolve its import"); + input.Text.assign(32, 0); + emit(input, 0, {0xFF, 0x15, 0xFA, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x100002000ull}, "RIP-relative import lost its full 64-bit address"); +} + void callbackAndRelocationRoots() { auto input = fixture(); ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020); @@ -294,8 +311,10 @@ std::vector elfFixture(const StrictReachabilityInput& input) { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 24, input.Entries.at(0)); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); write(bytes, 64, 1); @@ -325,7 +344,7 @@ void filterCallbackDataImports() { const auto bytes = elfFixture(input); for (const std::uint32_t relocationType : {1, 6}) { const std::vector references = {{"callbackData", {}, relocationType, 0x300, 0x2000, 0}, {"deadData", {}, relocationType, 0x318, 0x2008, 0}}; - const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr); + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); require(filtered.size() == 1 && filtered[0].Nid == references[0].Nid, "Strict ELF filter lost callback data or retained unreachable data"); } } @@ -336,7 +355,7 @@ void filterAndPltCompaction() { importThunk(input, 16, 0x2008); const auto bytes = elfFixture(input); const std::vector references = {{"live", {}, 7, 0x300, 0x2000, 0}, {"dead", {}, 7, 0x318, 0x2008, 0}}; - const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr); + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); require(filtered.size() == 1 && filtered[0].Nid == references[0].Nid, "Strict ELF filter did not remove only the dead PLT import"); for (std::size_t index = 0; index < references.size(); ++index) { const auto offset = index * 16; @@ -359,13 +378,13 @@ void filterAndPltCompaction() { require(section.RelaPltData.size() == 24 && section.DynSymData.size() == 48, "Compacted PLT retained a dead dynamic symbol"); auto truncated = bytes; truncated.resize(70); - requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, truncated, input.Text, input.TextVaddr); }, "Truncated ELF was accepted"); + requireFailure([&] { Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, truncated, input.Text, input.TextVaddr); }, "Truncated ELF was accepted"); auto exceptional = bytes; write(exceptional, 56, 3); write(exceptional, 176, 0x6474E550); write(exceptional, 184, 0x380); write(exceptional, 208, 8); - requireFailure([&] { Relinker::MakeStrictUnusedNidFilter()->Filter(references, exceptional, input.Text, input.TextVaddr); }, "Exception metadata was ignored"); + requireFailure([&] { Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, exceptional, input.Text, input.TextVaddr); }, "Exception metadata was ignored"); } void relativeRelocationTableBounds() { @@ -506,7 +525,7 @@ void sceDynlibDataOffsets() { } try { const std::vector references = {{"live", {}, 6, 0x900, 0x2000, 0}}; - const auto filtered = Relinker::MakeStrictUnusedNidFilter()->Filter(references, bytes, input.Text, input.TextVaddr); + const auto filtered = Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, bytes, input.Text, input.TextVaddr); if (filtered.size() != 1) failures += "SCE relocation target did not keep its import"; } catch (const std::exception& error) { failures += std::string("SCE strict filter: ") + error.what(); @@ -519,7 +538,7 @@ void sceDynlibDataOffsets() { require(Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(noSegment)->TargetOfSlot(0x2040) == 0x1010, "SCE relocation without PT_SCE_DYNLIBDATA was not read at its absolute file offset"); const std::vector references = {{"live", {}, 6, 0x900, 0x2000, 0}}; - require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, noSegment, input.Text, input.TextVaddr).size() == 1, + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, noSegment, input.Text, input.TextVaddr).size() == 1, "Strict filter lost an SCE relocation without PT_SCE_DYNLIBDATA"); auto jmprel = bytes; @@ -566,7 +585,7 @@ void sceDynlibDataOffsets() { write(overflow, 0x308, std::numeric_limits::max()); requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(overflow); }, "SCE relocation file offset overflow was accepted"); - requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, overflow, input.Text, input.TextVaddr); }, + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, overflow, input.Text, input.TextVaddr); }, "Strict filter accepted an overflowing SCE table offset"); auto duplicate = bytes; @@ -574,7 +593,7 @@ void sceDynlibDataOffsets() { write(duplicate, 288, 0x61000000); requireFailure([&] { (void)Relinker::UnusedNidFilter::BuildRelativeRelocationIndex(duplicate); }, "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); - requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); }, + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); auto sectionRelative = bytes; @@ -588,19 +607,19 @@ void sceDynlibDataOffsets() { write(sectionRelative, 0x358, 48); write(sectionRelative, 0x61C, 3); write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); - require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + require(Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); auto valuedSection = sectionRelative; write(valuedSection, 0x620, 0x10); - requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, valuedSection, input.Text, input.TextVaddr); }, "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); auto untyped = sectionRelative; write(untyped, 0x61C, 0); - requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, untyped, input.Text, input.TextVaddr); }, + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, untyped, input.Text, input.TextVaddr); }, "Strict filter accepted a RELATIVE relocation against a non-section symbol"); auto outside = sectionRelative; write(outside, 0x908, (std::uint64_t{2} << 32) | 8); - requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, outside, input.Text, input.TextVaddr); }, + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter(testProgress)->Filter(references, outside, input.Text, input.TextVaddr); }, "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } @@ -612,6 +631,7 @@ int main() { ud1DeadTail(); conditionalTailCall(); fallthroughAfterZeroPadding(); + addressSizeOverrideImport(); callbackAndRelocationRoots(); registerImportCall(); mergedJumpTable(); diff --git a/core/relinker/relinker/tests/TestProgress.hpp b/core/relinker/relinker/tests/TestProgress.hpp new file mode 100644 index 0000000000..2c8c9d120d --- /dev/null +++ b/core/relinker/relinker/tests/TestProgress.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class TestProgress final : public Domain::IProgress { +public: + std::vector Events; + std::string Messages; + + void Report(const Domain::ProgressEvent& event) override { Events.push_back(event); } + void Message(std::string_view message) override { Messages += message; } +}; + +inline TestProgress testProgress; diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index d314868311..171c44e1c0 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -1,4 +1,5 @@ from pathlib import Path +import struct import subprocess import sys import tempfile @@ -6,6 +7,10 @@ import tempfile from test_guest_intel_trampolines import main_fixture +MODES = ([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"], + ["--macos"], ["--macos", "--to-rosetta"]) + + def identification_cases(relinker, directory): image = main_fixture() cases = [("sysv", image, None)] @@ -21,7 +26,7 @@ def identification_cases(relinker, directory): data = bytearray(image) data[18:20] = value.to_bytes(2, "little") cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) - for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): + for mode, flags in enumerate(MODES): for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") @@ -35,15 +40,51 @@ def identification_cases(relinker, directory): assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) - assert output.read_bytes().startswith(b"MZ" if "--windows" in flags else b"\x7fELF"), (mode, name) + magic = b"\x7fELF" + if "--windows" in flags: + magic = b"MZ" + elif "--macos" in flags: + magic = b"\xcf\xfa\xed\xfe" + assert output.read_bytes().startswith(magic), (mode, name) + + +def structural_cases(relinker, directory): + fields = ( + (20, "I", (0, 2, 0xFFFFFFFF), "Unsupported ELF object version"), + (52, "H", (0, 63, 65, 65535), "Invalid ELF header size"), + ) + for mode, flags in enumerate(MODES): + for filter_level in (0, 1, 2): + for offset, encoding, values, error in fields: + for value in values: + case = Path(directory) / f"header-{mode}-{filter_level}-{offset}-{value}" + case.mkdir() + source = case / "input.elf" + output = case / "output.out" + registry = case / "output.registry.json" + data = main_fixture() + struct.pack_into("<" + encoding, data, offset, value) + source.write_bytes(data) + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", + f"unused-filter={filter_level}", *flags, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and error in result.stderr, (case.name, result) + assert source.read_bytes() == data, (case.name, "source changed") + assert not output.exists() and not registry.exists(), (case.name, "output written") def main(): relinker = Path(sys.argv[1]).resolve() + headerOnly = bytearray(64) + headerOnly[0:7] = b"\x7fELF\x02\x01\x01" + headerOnly[18:20] = (62).to_bytes(2, "little") + headerOnly[20:24] = (1).to_bytes(4, "little") + headerOnly[52:54] = (64).to_bytes(2, "little") cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(11) + b"\x3e" + bytes(45), "No PT_DYNAMIC segment found"), + ("header-only", bytes(headerOnly), "No PT_DYNAMIC segment found"), + ("header-only-sections", main_fixture()[:64], "FileByteOffset out of bounds"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: @@ -55,6 +96,7 @@ def main(): capture_output=True, text=True, timeout=20) assert result.returncode == 2 and error in result.stderr and not output.exists(), (name, result) identification_cases(relinker, directory) + structural_cases(relinker, directory) print("ELF header tests passed") diff --git a/core/relinker/relinker/tests/test_executables.py b/core/relinker/relinker/tests/test_executables.py new file mode 100644 index 0000000000..b975342de7 --- /dev/null +++ b/core/relinker/relinker/tests/test_executables.py @@ -0,0 +1,76 @@ +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol, needed_libraries +from test_guest_needed_modules import executable_with_needed + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-executables-") as directory: + work = Path(directory) + + def convert(case, windows, options, picker=None): + (case / "sce_module").mkdir(parents=True, exist_ok=True) + (case / "sce_module" / "bundled.prx").write_bytes(module_with_symbol(True)) + (case / "picker").mkdir(exist_ok=True) + (case / "picker" / "picker.bin").write_bytes(main_fixture() if picker is None else picker) + source = case / "eboot.bin" + source.write_bytes(main_fixture()) + output = case / "out" / ("app.exe" if windows else "app.elf") + output.parent.mkdir(exist_ok=True) + result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", *options, str(source), str(output)], + capture_output=True, text=True, timeout=60) + return result, output + + for windows in (False, True): + suffix = ".exe" if windows else ".elf" + case = work / f"{windows}-picker" + result, output = convert(case, windows, ["--executable", "picker/picker.bin"]) + assert result.returncode == 0, (result.stdout, result.stderr) + secondary = output.parent / ("picker.picker.bin" + suffix) + assert secondary.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), secondary + assert "Executable: picker/picker.bin\n" in result.stdout, result.stdout + assert f"Executable file: {secondary}\n" in result.stdout, result.stdout + assert f" |-- app{suffix}\n |-- picker.picker.bin{suffix}\n |-- libs/\n" in result.stdout, result.stdout + assert (output.parent / "picker.picker.bin.registry.json").is_file() + assert sorted(path.name for path in output.parent.rglob("*.guest.prx")) == ["bundled.prx.guest.prx"] + if not windows: + assert "$ORIGIN/app0/sce_module/bundled.prx.guest.prx" in needed_libraries(secondary.read_bytes()) + if windows and os.name == "nt": + for executable in (output, secondary): + run = subprocess.run([str(executable)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (executable, run.returncode, run.stdout, run.stderr) + + for options, message in ((["--executable", "../eboot.bin"], "inside the guest module parent directory"), + (["--executable", "picker"], "is not a file"), + (["--executable", "picker/missing.bin"], "is not a file"), + (["--executable", "eboot.bin"], "converted twice"), + (["--executable", "picker/picker.bin", "--executable", "picker/./picker.bin"], "converted twice"), + (["--module-dir", "picker", "--executable", "picker/picker.bin"], "in a guest module directory"), + (["--module-dir", "picker/", "--executable", "picker/picker.bin"], "in a guest module directory")): + case = work / f"{windows}-rejected-{len(message)}-{len(options)}" + result, output = convert(case, windows, options) + assert result.returncode == 2 and message in result.stderr, (options, result.stderr) + assert not output.exists(), options + + case = work / f"{windows}-needed" + (case / "Media" / "Modules").mkdir(parents=True) + (case / "Media" / "Modules" / "needed.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, ["--module-dir", "Media/Modules", "--executable", "picker/picker.bin"], executable_with_needed()) + if windows: + assert result.returncode == 2 and "Guest modules convert differently for picker/picker.bin" in result.stderr, result.stderr + assert not output.exists() + else: + assert result.returncode == 0, (result.stdout, result.stderr) + secondary = output.parent / "picker.picker.bin.elf" + assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(secondary.read_bytes()) + assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" not in needed_libraries(output.read_bytes()) + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_guest_gnu_hash.py b/core/relinker/relinker/tests/test_guest_gnu_hash.py new file mode 100644 index 0000000000..7b03aca665 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_gnu_hash.py @@ -0,0 +1,111 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_gnu_hash_analysis import name_hash +from test_guest_intel_trampolines import main_fixture +from test_guest_symbol_names import dynamic_tags, file_offset +from test_windows_import_modules import dynamic, executable, provider + + +HASH = 0xA00 +SYMBOLS = 0xC00 +STRINGS = 0xD00 + + +def module(names=("alpha", "bravo", "charlie"), bucket_count=3): + image = provider(42) + image.extend(bytes(0x1000 - len(image))) + struct.pack_into("> 5) % 64)) + if chains: + chains[-1] |= 1 + struct.pack_into("= len(image), "The grown program header table must be moved past the input" + assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr + assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the input program header table changed" image = fixture(6, with_frame=False) table_end = 64 + 6 * 56 @@ -112,16 +118,14 @@ def main(): assert elf[table_end:table_end + 56] == image[table_end:table_end + 56], "Bytes after the program header table changed" for with_frame in (False, True): - source = Path(directory) / ("insufficient-slots-" + str(with_frame) + ".elf") - output = source.with_suffix(".out") image = fixture(5, with_frame=with_frame) image[64 + 5 * 56:64 + 6 * 56] = bytes([0xa5]) * 56 - source.write_bytes(image) - result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) - assert result.returncode == 2, (result.returncode, result.stdout, result.stderr) - assert "Not enough program header slots: need 6, available 5" in result.stderr, result.stderr - assert not output.exists(), "Insufficient program header slots must not produce an executable" - assert source.read_bytes() == image, "Input changed while rejecting insufficient program header slots" + result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image) + headers = program_headers(elf) + assert len(headers) == (7 if with_frame else 6), headers + assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers + assert struct.unpack_from("= len(image), "The grown program header table must be moved past the input" + assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed" image = fixture(5, with_frame=False) headers = image[64:64 + 4 * 56] diff --git a/core/relinker/relinker/tests/test_linux_entry_argv.py b/core/relinker/relinker/tests/test_linux_entry_argv.py index 61c0426814..82c043c19f 100644 --- a/core/relinker/relinker/tests/test_linux_entry_argv.py +++ b/core/relinker/relinker/tests/test_linux_entry_argv.py @@ -56,11 +56,21 @@ def main(): result = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) + relocated_source = Path(directory) / "relocated-table.elf" + relocated = Path(directory) / "relocated-table.out" + image = argv_fixture() + struct.pack_into("= len(image), "The program header table was not moved" if sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64"): - output.chmod(0o755) - for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): - executed = subprocess.run([str(output), *arguments], capture_output=True, timeout=20) - assert executed.returncode == expected, (arguments, executed.returncode) + for executable in (output, relocated): + executable.chmod(0o755) + for arguments, expected in ((["Z"], -signal.SIGTRAP), (["Z", "extra"], -signal.SIGILL)): + executed = subprocess.run([str(executable), *arguments], capture_output=True, timeout=20) + assert executed.returncode == expected, (executable.name, arguments, executed.returncode) for address, error in ((0x7FF00000, None), (0x80000000, "Entry stub call exceeds rel32 range")): source.write_bytes(far_load_fixture(address)) far = Path(directory) / ("far-%x.elf" % address) diff --git a/core/relinker/relinker/tests/test_linux_load_alignment.py b/core/relinker/relinker/tests/test_linux_load_alignment.py index d05c9a9230..f3b0c356b3 100644 --- a/core/relinker/relinker/tests/test_linux_load_alignment.py +++ b/core/relinker/relinker/tests/test_linux_load_alignment.py @@ -53,7 +53,7 @@ def main(): alignment = max(segment[7] for segment in segments) if alignment != 0x4000 or first[3] % alignment != 0 or first[7] != alignment: raise AssertionError(("first PT_LOAD is not aligned to the largest segment alignment", segments)) - assert "output.elf\nlibs/\n *.prx\napp0/" in result.stdout, result.stdout + assert " |-- output.elf\n |-- libs/\n |-- *.prx (system libraries)\n |-- app0/" in result.stdout, result.stdout named_exe = Path(directory) / "linux.EXE" warning = subprocess.run([str(relinker), "--skip-sce-module", str(source), str(named_exe)], capture_output=True, text=True, timeout=20) assert warning.returncode == 0, (warning.stdout, warning.stderr) diff --git a/core/relinker/relinker/tests/test_long_paths.py b/core/relinker/relinker/tests/test_long_paths.py new file mode 100644 index 0000000000..e5c5bb857f --- /dev/null +++ b/core/relinker/relinker/tests/test_long_paths.py @@ -0,0 +1,43 @@ +"""Convert an input and write outputs whose paths exceed the 260 characters of the Windows MAX_PATH limit.""" + +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture + + +def long_paths_enabled(): + if os.name != "nt": + return True + import winreg + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\FileSystem") as key: + return winreg.QueryValueEx(key, "LongPathsEnabled")[0] == 1 + except OSError: + return False + + +def main(): + relinker = Path(sys.argv[1]).resolve() + if not long_paths_enabled(): + print("skipped, Windows long paths are disabled: LongPathsEnabled is not 1") + sys.exit(77) + with tempfile.TemporaryDirectory(prefix="anyps5-long-path-") as directory: + work = Path(directory).joinpath(*["long-path-component-" + "x" * 40] * 5) + work.mkdir(parents=True) + source = work / "input.elf" + source.write_bytes(main_fixture()) + for options, name in (([], "output.elf"), (["--windows", "--registry"], "output.exe")): + output = work / name + assert len(str(output)) > 260, output + result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=20) + assert result.returncode == 0 and output.is_file(), (options, result) + print("Long path tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_weak_imports.py b/core/relinker/relinker/tests/test_weak_imports.py new file mode 100644 index 0000000000..1141a74c67 --- /dev/null +++ b/core/relinker/relinker/tests/test_weak_imports.py @@ -0,0 +1,75 @@ +"""Check that weak undefined imports stay weak on Linux and bind to null on Windows when no module exports them.""" + +import os +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +from test_windows_import_modules import executable, provider + +SHT_DYNSYM = 11 +STB_GLOBAL = 1 +STB_WEAK = 2 + + +def weak_executable(owner, symbol): + image = executable(owner, symbol) + image[0x4898 + 4] = (STB_WEAK << 4) | 2 + code = (b'\x48\x8b\x05' + struct.pack('> 4 + return bindings + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: + work = Path(directory) + + for name, image, symbol, expected in (('linux-weak', weak_executable('a.prx', 'absent#A#B'), 'absent', STB_WEAK), + ('linux-global', executable('a.prx', 'absent#A#B'), 'absent', STB_GLOBAL)): + source = work / (name + '.elf') + source.write_bytes(image) + output = work / (name + '.out') + result = subprocess.run([str(relinker), '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + bindings = import_bindings(output.read_bytes()) + assert bindings.get(symbol) == expected, (name, bindings) + + for name, symbol, expected in (('windows-weak-missing', 'absent#A#B', 33), + ('windows-weak-present', 'shared#A#B', 11)): + case = work / name + (case / 'prx').mkdir(parents=True) + (case / 'prx' / 'a.prx').write_bytes(provider(11)) + source = case / 'input.elf' + source.write_bytes(weak_executable('a.prx', symbol)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if os.name == 'nt': + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + print('Weak import tests passed') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py new file mode 100644 index 0000000000..cd5d8cd5f2 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -0,0 +1,147 @@ +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture +from test_windows_import_modules import executable, provider + + +def pe_headers(data): + pe = struct.unpack_from(" +#include +#include +#include + +namespace { + +class BrowserProgress final : public Domain::IProgress { +public: + explicit BrowserProgress(emscripten::val callback) : callback(std::move(callback)) {} + + void Report(const Domain::ProgressEvent& event) override { + auto report = emscripten::val::object(); + report.set("stage", static_cast(event.Stage)); + report.set("input", event.Input); + report.set("completed", static_cast(event.Completed)); + report.set("total", static_cast(event.Total)); + callback(report); + } + + void Message(std::string_view message) override { + auto report = emscripten::val::object(); + report.set("message", std::string(message)); + callback(report); + } + +private: + emscripten::val callback; +}; + +std::vector ReadBytes(const emscripten::val& bytes, std::size_t& remaining) { + if (!bytes.instanceof(emscripten::val::global("Uint8Array"))) throw std::invalid_argument("Expected Uint8Array input"); + const auto length = bytes["length"].as(); + if (length > remaining) throw std::invalid_argument("Executable, modules and icon exceed the 256 MiB conversion limit"); + remaining -= length; + std::vector result(length); + if (length != 0) emscripten::val(emscripten::typed_memory_view(length, result.data())).call("set", bytes); + return result; +} + +emscripten::val CopyBytes(const std::vector& bytes) { + return emscripten::val::global("Uint8Array").new_(emscripten::val(emscripten::typed_memory_view(bytes.size(), bytes.data()))); +} + +emscripten::val Convert(const emscripten::val& request, const emscripten::val& callback) { + auto result = emscripten::val::object(); + try { + const auto target = request["target"].as(); + if (target != "windows" && target != "linux") throw std::invalid_argument("Unknown target system"); + const auto name = request["name"].as(); + if (name.empty() || name.find_first_of("/\\") != std::string::npos || name == "." || name == "..") throw std::invalid_argument("Invalid executable name"); + std::size_t remaining = 256 * 1024 * 1024; + Relinker::ConversionInput input; + input.InputPath = Io::NativePath("/input/" + name); + input.OutputPath = Io::NativePath("/output/" + request["outputName"].as()); + if (input.OutputPath.parent_path() != "/output") throw std::invalid_argument("Invalid output name"); + input.ModuleRoot = "/input"; + input.Executable = ReadBytes(request["executable"], remaining); + const auto modules = request["modules"]; + const auto count = modules["length"].as(); + if (count > 4096) throw std::invalid_argument("Too many modules"); + for (unsigned index = 0; index < count; ++index) { + const auto module = modules[index]; + const auto path = Io::NativePath(module["name"].as()); + if (path.is_absolute() || path.lexically_normal() != path || path.begin() == path.end() || *path.begin() != "sce_module") throw std::invalid_argument("Invalid module path"); + input.Modules.push_back({Io::NativePath("/input") / path, ReadBytes(module["bytes"], remaining)}); + } + input.RuntimeDirectories.push_back("/input/sce_module"); + if (!request["icon"].isNull()) input.Icon = ReadBytes(request["icon"], remaining); + Relinker::ConversionOptions options; + options.Target = target == "windows" ? Relinker::TargetSystem::Windows : Relinker::TargetSystem::Linux; + options.WindowsGui = false; + options.WindowsDiagnostics = target == "windows"; + options.ToIntel = request["toIntel"].as(); + BrowserProgress progress(callback); + auto output = Relinker::Conversion(progress).Convert(std::move(input), options); + result.set("executable", CopyBytes(output.Executable)); + result.set("diagnostics", output.WindowsDiagnostics); + auto artifacts = emscripten::val::array(); + for (const auto& module : output.Modules) { + auto artifact = emscripten::val::object(); + artifact.set("name", Io::Utf8Path(module.Path.lexically_relative("/output"))); + artifact.set("bytes", CopyBytes(module.Bytes)); + artifacts.call("push", artifact); + } + result.set("modules", artifacts); + result.set("ok", true); + } catch (const std::exception& error) { + result.set("ok", false); + result.set("error", std::string(error.what())); + } + return result; +} + +} + +EMSCRIPTEN_BINDINGS(relinker) { + emscripten::function("Convert", &Convert); +} diff --git a/core/relinker/wasm/CMakeLists.txt b/core/relinker/wasm/CMakeLists.txt new file mode 100644 index 0000000000..b92335d011 --- /dev/null +++ b/core/relinker/wasm/CMakeLists.txt @@ -0,0 +1,28 @@ +add_executable(relinker_wasm Bindings.cpp) +target_link_libraries(relinker_wasm PRIVATE relinker_core) +target_compile_options(relinker_wasm PRIVATE -fexceptions) +target_link_options(relinker_wasm PRIVATE + --bind + --no-entry + -fexceptions + -sMODULARIZE=1 + -sEXPORT_ES6=1 + -sEXPORT_NAME=createRelinker + -sALLOW_MEMORY_GROWTH=1 + -sMAXIMUM_MEMORY=2147483648 + -sSTACK_SIZE=1048576 + -sFILESYSTEM=0 + -sENVIRONMENT=web,worker,node +) +set_target_properties(relinker_wasm PROPERTIES + SUFFIX ".mjs" + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web/native" +) + +find_package(Python3 REQUIRED COMPONENTS Interpreter) +add_custom_target(relinker_web + COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/tools/build_web.py" --build "${CMAKE_BINARY_DIR}" + DEPENDS relinker_wasm + VERBATIM +) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 6d3d68e2db..d4ff8cefaa 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -12,6 +12,7 @@ function(add_shader_recompiler target runtimeTarget) set(SPIRV_WERROR OFF) set(BUILD_SHARED_LIBS OFF) add_subdirectory("${PROJECT_SOURCE_DIR}/3rdparty/SPIRV-Tools" "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools" EXCLUDE_FROM_ALL) + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PatchSpirvTools.cmake") endif() if(NOT TARGET glslang) diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 08f5d79c55..6f51a855af 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -106,6 +106,8 @@ private: append(key, tessellation != nullptr); if (tessellation == nullptr) return; for (const auto value : {tessellation->inputControlPoints, tessellation->outputControlPoints, tessellation->domain, tessellation->partitioning, tessellation->outputTopology}) append(key, value); + if (request.shader.stage == ShaderStage::Local) return; + append(key, tessellation->patchesPerGroup); } template @@ -192,6 +194,7 @@ private: append(key, value.fetchAttribReg); append(key, value.fetchBufferReg); append(key, value.fetchEmbedded); + append(key, value.paClVsOutCntl); if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count"); append(key, value.resourcesNum); for (std::uint32_t i = 0; i < value.resourcesNum; ++i) { @@ -244,6 +247,7 @@ private: append(key, value.registrationModules); append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); + append(key, value.float16DenormPreserve); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 84e55eeaf5..217d54eee7 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -243,6 +243,14 @@ ColorExportPacking readColorExportPacking(Reader& reader) { return static_cast(value); } +RasterPrimitive readRasterPrimitive(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(RasterPrimitive::Rectangle)) { + throw std::runtime_error("RequestSerializer: invalid RasterPrimitive value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -326,6 +334,8 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.dualSourceBlend); writer.WriteBool(info.alphaToCoverage); writer.WriteBool(info.stencilRefExportEnable); + writer.WriteU8(static_cast(info.rasterPrimitive)); + writer.WriteBool(info.layerExported); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -375,6 +385,8 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 15u) info.dualSourceBlend = reader.ReadBool(); if (version >= 16u) info.alphaToCoverage = reader.ReadBool(); if (version >= 17u) info.stencilRefExportEnable = reader.ReadBool(); + if (version >= 19u) info.rasterPrimitive = readRasterPrimitive(reader); + if (version >= 23u) info.layerExported = 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) | @@ -402,9 +414,10 @@ void writeVertexInfo(Writer& writer, const ShaderVertexStageInfo& info) { writer.WriteU32(info.fetchAttribReg); writer.WriteU32(info.fetchBufferReg); writer.WriteBool(info.fetchEmbedded); + writer.WriteU32(info.paClVsOutCntl); } -ShaderVertexStageInfo readVertexInfo(Reader& reader) { +ShaderVertexStageInfo readVertexInfo(Reader& reader, std::uint32_t version) { ShaderVertexStageInfo info{}; for (auto& resource : info.resources) { for (std::uint32_t& value : resource.fields) { @@ -421,6 +434,7 @@ ShaderVertexStageInfo readVertexInfo(Reader& reader) { info.fetchAttribReg = reader.ReadU32(); info.fetchBufferReg = reader.ReadU32(); info.fetchEmbedded = reader.ReadBool(); + if (version >= 20u) info.paClVsOutCntl = reader.ReadU32(); return info; } @@ -508,15 +522,17 @@ void writeTessellationConfiguration(Writer& writer, const TessellationConfigurat writer.WriteU32(configuration.domain); writer.WriteU32(configuration.partitioning); writer.WriteU32(configuration.outputTopology); + writer.WriteU32(configuration.patchesPerGroup); } -TessellationConfiguration readTessellationConfiguration(Reader& reader) { +TessellationConfiguration readTessellationConfiguration(Reader& reader, std::uint32_t version) { TessellationConfiguration configuration{}; configuration.inputControlPoints = reader.ReadU32(); configuration.outputControlPoints = reader.ReadU32(); configuration.domain = reader.ReadU32(); configuration.partitioning = reader.ReadU32(); configuration.outputTopology = reader.ReadU32(); + if (version >= 22u) configuration.patchesPerGroup = reader.ReadU32(); return configuration; } @@ -565,7 +581,7 @@ GuestContext readGuestContext(Reader& reader, DeserializedRequest& result, std:: context.pixel = result.pixel; } if (reader.ReadBool()) { - result.vertex = readVertexInfo(reader); + result.vertex = readVertexInfo(reader, version); context.vertex = result.vertex; } const auto regionCount = reader.ReadU64(); @@ -719,7 +735,7 @@ GraphicsCompileContext readGraphicsCompileContext(Reader& reader, DeserializedGr graphics.mesh = storage.mesh; } if (reader.ReadBool()) { - storage.tessellation = readTessellationConfiguration(reader); + storage.tessellation = readTessellationConfiguration(reader, version); graphics.tessellation = storage.tessellation; } storage.draw.indexAddress = reader.ReadU64(); @@ -736,7 +752,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(18u); + writer.WriteU32(23u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -759,6 +775,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.registrationModules); writer.WriteU32(request.target.registrationWordLimit); writer.WriteBool(request.target.vertexSubgroups); + writer.WriteBool(request.target.float16DenormPreserve); return base64Encode(buffer); } @@ -767,7 +784,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 > 18u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 23u) 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); @@ -791,6 +808,7 @@ 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(); return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp index adb325a3ed..435a5c0b1a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBlock.hpp @@ -5,10 +5,37 @@ #include #include #include +#include #include namespace ShaderRecompiler { +template +class IrSsaValues { +public: + IrValue*& operator[](std::size_t index) { + if (storage == nullptr) storage = std::make_unique>(); + return (*storage)[index]; + } + + IrValue* operator[](std::size_t index) const { return values()[index]; } + bool HasStorage() const { return storage != nullptr; } + void fill(IrValue* value) { + if (value == nullptr) { storage.reset(); return; } + if (storage == nullptr) storage = std::make_unique>(); + storage->fill(value); + } + auto begin() const { return values().begin(); } + auto end() const { return values().end(); } + +private: + const std::array& values() const { + static const std::array empty{}; + return storage != nullptr ? *storage : empty; + } + std::unique_ptr> storage; +}; + class IrBlock { public: explicit IrBlock(std::uint32_t id); @@ -29,10 +56,10 @@ public: void AddPredecessor(IrBlock* block); void AddSuccessor(IrBlock* block); - std::array ssaScalarValues{}; - std::array ssaThreadBitScalarValues{}; - std::array ssaScalarMaskTags{}; - std::array ssaVectorValues{}; + IrSsaValues ssaScalarValues{}; + IrSsaValues ssaThreadBitScalarValues{}; + IrSsaValues ssaScalarMaskTags{}; + IrSsaValues ssaVectorValues{}; [[nodiscard]] const std::vector& Predecessors() const; [[nodiscard]] const std::vector& Successors() const; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index eed5b95514..f9df378d48 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -62,6 +62,7 @@ struct ResourceBlock { IrValue* condition = nullptr; std::vector successors; std::vector sources; + std::vector images; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 67eedc48cc..3a5e28dd46 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -28,7 +28,9 @@ struct IrProgramMetadata { std::uint32_t tessellationLocalStride = 0; std::uint32_t vectorWindowBase = 0; std::uint32_t vectorWindowSize = 0; + bool vectorWindowIndexedWrites = false; std::uint32_t tessellationControlStride = 0; + std::uint32_t tessellationPatchStride = 0; }; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 25e0805e5b..79cbff90ce 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -135,6 +135,10 @@ inline bool FoldsTexelOffsets(const SamplerResource& sampler) { return (sampler.uses & SamplerUseOffset) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; } +inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { + return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; +} + struct SampledResourcePair { std::uint32_t image = 0; std::uint32_t sampler = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index aa1f183eea..8879b166be 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -35,6 +35,8 @@ struct ShaderInfo { bool bdaWrites = false; bool usesFaultBuffer = false; bool dispatchThreadLimit = false; + bool rasterPrimitive = false; + bool layerExported = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 2c5b7c3792..96c7d51533 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -13,7 +13,8 @@ enum class TessellationAttribute { ControlOutput, EvaluationInput, PatchOutput, - Factor + Factor, + PatchInput }; enum class TessellationBaseKind { @@ -43,6 +44,8 @@ enum class StageInputKind { DispatchThreadLimit, Parameter, HelperInvocation, + RasterPrimitive, + LayerExported, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp index 26739b94f6..8ffdb9c68a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrBlock.hpp" #include "IntermediateRepresentation/IrMetadata.hpp" +#include #include #include #include @@ -29,6 +30,7 @@ public: CompiledShaderInfo TakeCompiledInfo() &&; IrValue& CreateValue(IrOpcode opcode, IrType type, std::uint64_t flags); void SetEntryBlock(IrBlock& block); + std::size_t ReclaimUnusedValues(); [[nodiscard]] std::vector& BlockOrder(); [[nodiscard]] const std::vector& BlockOrder() const; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp index 3f45934d44..3698a7afc1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp @@ -116,7 +116,8 @@ IrValue* IrBuilder::vectorWindowIndex(VectorReg reg) { } IrValue& IrBuilder::GetVectorReg(VectorReg reg) { - if (IrValue* index = vectorWindowIndex(reg)) return Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); + IrValue* index = vectorWindowIndex(reg); + if (index != nullptr && program.Metadata().vectorWindowIndexedWrites) return Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); return Emit(IrOpcode::GetVectorRegister, IrType::U32, {&operand}); } @@ -124,7 +125,7 @@ IrValue& IrBuilder::GetVectorReg(VectorReg reg) { void IrBuilder::SetVectorReg(VectorReg reg, IrValue& value) { if (IrValue* index = vectorWindowIndex(reg)) { (void)Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &value}); - return; + if (program.Metadata().vectorWindowIndexedWrites) return; } IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); (void)Emit(IrOpcode::SetVectorRegister, IrType::Void, {&operand, &value}); diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 6a9d16ec0f..7ad5b937ee 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -207,6 +207,45 @@ std::string valueToString(const std::map& ids, cons } +std::size_t IrProgram::ReclaimUnusedValues() { + std::unordered_set live; + std::vector pending; + const auto keep = [&](IrValue* value) { if (value != nullptr && live.insert(value).second) pending.push_back(value); }; + const auto keepBlocks = [&](const auto& storage) { + for (const auto& block : storage) { + for (IrValue* instruction : block->Instructions()) keep(instruction); + for (IrValue* value : block->ssaScalarValues) keep(value); + for (IrValue* value : block->ssaVectorValues) keep(value); + for (IrValue* value : block->ssaThreadBitScalarValues) keep(value); + for (IrValue* value : block->ssaScalarMaskTags) keep(value); + } + }; + keepBlocks(blocks); + keepBlocks(resourcePlan.blockStorage); + for (const auto& value : resourcePlan.valueStorage) keep(value.get()); + for (const auto& source : resourcePlan.descriptorSources) for (IrValue* word : source.dwords) keep(word); + for (const auto& read : resourcePlan.srtReads) keep(read.value); + for (const auto& block : resourcePlan.controlFlow) keep(block.condition); + for (IrValue* value : resourcePlan.uniformFill.values) keep(value); + for (const auto& block : metadata.blockInfo) { keep(block.condition); keep(block.indirectTarget); } + for (IrValue* value : metadata.dynamicReads) keep(value); + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) keep(value->Argument(index)); + } + for (const auto& value : values) { + if (live.contains(value.get())) continue; + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) value->ReplaceArgument(index, nullptr); + } + for (const auto& value : values) { + if (!live.contains(value.get()) && value->HasUses()) throw std::runtime_error("an unused IR value has an external user"); + } + const auto removed = std::erase_if(values, [&](const auto& value) { return !live.contains(value.get()); }); + values.shrink_to_fit(); + return removed; +} + std::vector>& IrProgram::Blocks() { return blocks; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index 6be55fb231..d2df4ed3cc 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,6 +14,8 @@ public: void Fold(IrProgram& program, std::span blocks) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; + private: [[nodiscard]] bool tryFoldValue(IrProgram& program, IrValue& value) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index ba65e32427..f5029b374d 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -112,6 +112,7 @@ struct ShaderTessellationInputInfo { std::uint32_t domain = 0; std::uint32_t partitioning = 0; std::uint32_t outputTopology = 0; + std::uint32_t patchesPerGroup = 256; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index b70fbab7b8..3505f640f4 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -321,9 +321,25 @@ std::optional packedField(std::uint32_t offset, std::uint32_t count return static_cast(field - packedFields.begin()); } +bool isPackedAncillary(const IrValue& builtin) { + auto& kind = resolveArg(builtin, 0); + return isImmediate(kind, IrType::U32) && static_cast(kind.ImmediateU32()) == StageInputKind::PackedAncillary; +} + +IrValue& insertBefore(IrProgram& program, IrValue& next, IrOpcode opcode, IrValue& lhs, IrValue& rhs) { + IrValue& created = program.CreateValue(opcode, IrType::U32); + created.AddArgument(&lhs); + created.AddArgument(&rhs); + next.Parent()->InsertInstructionBefore(&next, &created); + return created; +} + +IrValue& insertBuiltin(IrProgram& program, IrBuilder& builder, IrValue& next, StageInputKind kind) { + return insertBefore(program, next, IrOpcode::GetBuiltin, builder.Constant(static_cast(kind)), builder.Constant(0u)); +} + void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { - auto& kind = resolveArg(ancillary, 0); - if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + if (!isPackedAncillary(ancillary)) { return; } const std::vector uses = ancillary.OperandUses(); @@ -368,22 +384,14 @@ void lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancil } std::array fields {}; const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr && index == 1u) fields[index] = &insertBefore(program, ancillary, IrOpcode::IMul32, insertBuiltin(program, builder, ancillary, StageInputKind::Layer), insertBuiltin(program, builder, ancillary, StageInputKind::LayerExported)); if (fields[index] == nullptr) { - const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; - IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); - created.AddArgument(&builder.Constant(static_cast(builtin))); - created.AddArgument(&builder.Constant(0u)); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - fields[index] = &created; + fields[index] = &insertBuiltin(program, builder, ancillary, StageInputKind::SampleId); } return *fields[index]; }; const auto emit = [&](IrOpcode opcode, IrValue& lhs, IrValue& rhs) -> IrValue& { - IrValue& created = program.CreateValue(opcode, IrType::U32); - created.AddArgument(&lhs); - created.AddArgument(&rhs); - ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); - return created; + return insertBefore(program, ancillary, opcode, lhs, rhs); }; if (!forwarded.empty()) { IrValue& sample = emit(IrOpcode::ShiftLeftLogical32, field(0u), builder.Constant(8u)); @@ -475,10 +483,6 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, builder.Constant(static_cast(operand.ImmediateU16()))); return true; } - if (operand.Opcode() == IrOpcode::ConvertU16U32) { - replaceWith(value, *operand.Argument(0)); - return true; - } return false; } case IrOpcode::ConvertU8U32: { @@ -495,10 +499,6 @@ bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { replaceWith(value, builder.Constant(static_cast(operand.ImmediateU8()))); return true; } - if (operand.Opcode() == IrOpcode::ConvertU8U32) { - replaceWith(value, *operand.Argument(0)); - return true; - } return false; } case IrOpcode::CompositeExtractU64: return foldCompositeExtract(builder, value, IrOpcode::CompositeConstructU64, 2); @@ -846,4 +846,25 @@ void ConstantFolder::Fold(IrProgram& program) const { Fold(program, program.BlockOrder()); } +bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { + std::vector words; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::GetBuiltin && inst->HasUses() && isPackedAncillary(*inst)) { + words.push_back(inst); + } + } + } + IrBuilder builder(program); + for (IrValue* word : words) { + IrValue& primitive = insertBuiltin(program, builder, *word, StageInputKind::RasterPrimitive); + IrValue& sample = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, insertBuiltin(program, builder, *word, StageInputKind::SampleId), builder.Constant(8u)); + IrValue& exportedLayer = insertBefore(program, *word, IrOpcode::IMul32, insertBuiltin(program, builder, *word, StageInputKind::Layer), insertBuiltin(program, builder, *word, StageInputKind::LayerExported)); + IrValue& layer = insertBefore(program, *word, IrOpcode::ShiftLeftLogical32, exportedLayer, builder.Constant(16u)); + IrValue& fields = insertBefore(program, *word, IrOpcode::BitwiseOr32, sample, layer); + word->ReplaceAllUsesWith(&insertBefore(program, *word, IrOpcode::BitwiseOr32, primitive, fields)); + } + return !words.empty(); +} + } diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 5951e78778..6081365137 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -175,7 +175,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh continue; } const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseOffset | SamplerUseNonZeroLod); - if (unsupported != 0u) { + if (unsupported != 0u && !SamplesBaseLevelIfUnnormalized(sampler)) { failUnnormalized(UnnormalizedUseReason(unsupported)); } if (sampler.depthCompare) { @@ -259,6 +259,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto unnormalized = ProveUnnormalized(info, snapshot); for (std::uint32_t index = 0; index < info.samplers.size(); ++index) { if (FoldsTexelOffsets(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::FoldOffsetBase + index, unnormalized.samplers[index] ? 1u : 0u}); + if (SamplesBaseLevelIfUnnormalized(info.samplers[index])) plan.specialization.push_back({PipelineSpecialization::UnnormalizedBaseLevelBase + index, unnormalized.samplers[index] ? 1u : 0u}); } std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 53118b192a..ce23f6fb6c 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -487,6 +487,11 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, if (descriptor.dwordCount != 8u) { throw std::runtime_error("image descriptor has an invalid width"); } + const auto activity = plan.descriptorSources.size() + i; + if (activity < activeSources.size() && activeSources[activity] == 0u) { + descriptor = DescriptorValue{}; + descriptor.dwordCount = 8u; + } if (!validImageDescriptor(descriptor, image.r128) && !nullImageDescriptor(descriptor)) { throw std::runtime_error("runtime image descriptor is invalid"); } @@ -501,10 +506,11 @@ void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, std::uint32_t colorCompareReference(IrBufferFormat format) { switch (format) { - case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format16_16UNorm: + case IrBufferFormat::Format16_16UNorm: case IrBufferFormat::Format11_11_10UNorm: case IrBufferFormat::Format10_11_11UNorm: case IrBufferFormat::Format2_10_10_10UNorm: - case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format16_16_16_16UNorm: + case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format16_16_16_16UNorm: return EmulatedCompare::ReferenceUnorm; + case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8_8UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format8Srgb: case IrBufferFormat::Format8_8Srgb: case IrBufferFormat::Format8_8_8_8Srgb: return EmulatedCompare::ReferenceZero; case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: case IrBufferFormat::Format8_8SNorm: case IrBufferFormat::Format16_16SNorm: @@ -551,8 +557,8 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; - const bool eightBitSrgb = format == IrBufferFormat::Format8Srgb || format == IrBufferFormat::Format8_8Srgb || format == IrBufferFormat::Format8_8_8_8Srgb; - if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u && !eightBitSrgb) { + const bool constantZero = reference == EmulatedCompare::ReferenceZero; + if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u && !constantZero) { throw std::runtime_error("bilinear cube comparison requires DISABLE_CUBE_WRAP to avoid seamless face filtering"); } const auto addressMode = [](std::uint32_t clamp) { @@ -566,7 +572,7 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho const auto borderType = (words[3] >> 30u) & 0x3u; const bool border = addressX == EmulatedCompare::AddressBorder || addressY == EmulatedCompare::AddressBorder; if (border && borderType == 3u) throw std::runtime_error("comparison sampling of a color texture with a border color table is not implemented"); - if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); + if (!constantZero && (magFilter != minFilter || magFilter > 1u)) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); if (unnormalized) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates"); const auto state = EmulatedCompare::Enabled | (function << EmulatedCompare::FunctionShift) | (magFilter == 1u ? EmulatedCompare::Linear : 0u) | (addressX << EmulatedCompare::ClampXShift) | (addressY << EmulatedCompare::ClampYShift) | (border && borderType == 2u ? EmulatedCompare::BorderWhite : 0u); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 1ca667d979..675f7d561f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -114,11 +114,21 @@ public: } SplitDescriptorPhis(); PlanIndirectImages(); + std::unordered_map blockIndices; + for (const IrBlock* block : m_program.BlockOrder()) { + blockIndices.emplace(block, static_cast(blockIndices.size())); + } + m_blockSources.assign(blockIndices.size(), {}); + m_blockImages.assign(blockIndices.size(), {}); for (auto& block : m_program.Blocks()) { + const auto found = blockIndices.find(block.get()); + m_currentBlock = found == blockIndices.end() ? std::numeric_limits::max() : found->second; for (IrValue* inst : block->Instructions()) { Collect(*inst); } } + m_currentBlock = std::numeric_limits::max(); + PlanResourceControlFlow(); if (m_bdaTraces > bdaTraceLimit) std::fprintf(stderr, "[bda] %u more address accesses in this program not shown\n", m_bdaTraces - bdaTraceLimit); LinkImageAliases(); for (const auto& patch : m_handlePatches) { @@ -241,13 +251,104 @@ private: same = same && EquivalentValue(m_program.Resources(), current.dwords[i], descriptor.dwords[i]); } if (same) { + NoteBlockSource(candidate); return candidate; } } m_sources.push_back(descriptor); + NoteBlockSource(static_cast(m_sources.size() - 1)); return static_cast(m_sources.size() - 1); } + void NoteBlockImage(std::uint32_t image) { + if (m_currentBlock >= m_blockImages.size()) { + return; + } + auto& images = m_blockImages[m_currentBlock]; + if (std::ranges::find(images, image) == images.end()) { + images.push_back(image); + } + } + + void NoteBlockSource(std::uint32_t source) { + if (m_currentBlock >= m_blockSources.size()) { + return; + } + auto& sources = m_blockSources[m_currentBlock]; + if (std::ranges::find(sources, source) == sources.end()) { + sources.push_back(source); + } + } + + void PlanResourceControlFlow() { + const auto& order = m_program.BlockOrder(); + const auto& info = m_program.Metadata().blockInfo; + if (order.empty() || order.size() != info.size()) { + return; + } + std::unordered_map indexOf; + std::unordered_map idToIndex; + for (std::uint32_t index = 0; index < order.size(); index++) { + indexOf.emplace(order[index], index); + idToIndex.emplace(info[index].id, index); + } + std::vector controlFlow(order.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + auto& block = controlFlow[index]; + const auto& terminator = info[index].terminator; + const auto target = [&](std::uint32_t id) { return idToIndex.find(id); }; + const auto trueEdge = target(terminator.trueBlock); + const auto falseEdge = target(terminator.falseBlock); + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && info[index].condition != nullptr && trueEdge != idToIndex.end() && falseEdge != idToIndex.end()) { + block.condition = info[index].condition; + block.successors = {trueEdge->second, falseEdge->second}; + } else if (terminator.kind != TerminatorKind::Return) { + for (const IrBlock* successor : order[index]->Successors()) { + const auto found = indexOf.find(successor); + if (found == indexOf.end()) { + return; + } + block.successors.push_back(found->second); + } + } + block.sources = m_blockSources[index]; + block.images = m_blockImages[index]; + } + std::vector unconditional(order.size()); + for (std::uint32_t index = 0; index < order.size() && unconditional[index] == 0u;) { + unconditional[index] = 1u; + if (controlFlow[index].successors.size() != 1u) { + break; + } + index = controlFlow[index].successors.front(); + } + std::vector reached(m_sources.size()); + std::vector reachedImages(m_info.images.size()); + for (std::uint32_t index = 0; index < order.size(); index++) { + if (unconditional[index] == 0u) { + continue; + } + for (const auto source : controlFlow[index].sources) { + reached[source] = 1u; + } + for (const auto image : controlFlow[index].images) { + reachedImages[image] = 1u; + } + } + bool guarded = false; + for (const auto& block : controlFlow) { + for (const auto source : block.sources) { + guarded = guarded || reached[source] == 0u; + } + for (const auto image : block.images) { + guarded = guarded || reachedImages[image] == 0u; + } + } + if (guarded) { + m_program.Resources().controlFlow = std::move(controlFlow); + } + } + static bool immediateU32(IrValue* value, std::uint32_t& result) { value = value->Resolve(); if (!value->HasImmediate() || value->Type() != IrType::U32) { @@ -1086,7 +1187,7 @@ private: if (resource == std::numeric_limits::max()) fail("buffer resource limit exceeded"); AddMemoryPatch(memoryIndex, resource, 0u, false); memory.gpuDescriptor = false; - m_info.usesDma = m_info.usesDma || access == BufferAccess::Atomic; + m_info.usesFaultBuffer = m_info.usesFaultBuffer || access == BufferAccess::Atomic; return true; } memory.gpuDescriptor = true; @@ -1330,6 +1431,7 @@ private: GetHandle(inst.Argument(0), IrOpcode::GetImageResource, 8, handle, source); } resource = AddImage(source, memory, op, flags.pc); + NoteBlockImage(resource); if (resource == std::numeric_limits::max()) { fail("image resource limit exceeded"); } @@ -1384,6 +1486,9 @@ private: ShaderInfo m_info; IrBuilder m_builder; std::vector m_sources; + std::vector> m_blockSources; + std::vector> m_blockImages; + std::uint32_t m_currentBlock = std::numeric_limits::max(); std::vector m_handlePatches; std::vector m_memoryPatches; // APS5_TRACE_BDA: address accesses seen by Collect (the first bdaTraceLimit are printed). diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index e9481485b6..419c219764 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -143,6 +143,8 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu } case StageInputKind::Layer: case StageInputKind::SampleId: + case StageInputKind::RasterPrimitive: + case StageInputKind::LayerExported: if (program.Resources().stage != IrShaderStage::Pixel || component != 0u) { return Fail("typed pixel scalar input is invalid"); } @@ -386,6 +388,12 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::DispatchThreadLimit: info.dispatchThreadLimit = true; break; + case StageInputKind::RasterPrimitive: + info.rasterPrimitive = true; + break; + case StageInputKind::LayerExported: + info.layerExported = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index bc89e71e9e..690bad731a 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -63,16 +63,23 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span active; if (evaluateFlat) { - active.assign(program.descriptorSources.size(), 1u); + active.assign(program.descriptorSources.size() + program.info.images.size(), 1u); } if (evaluateFlat && !program.controlFlow.empty()) { for (const auto& block : program.controlFlow) { for (const auto source : block.sources) { active.at(source) = 0u; } + for (const auto image : block.images) { + active.at(program.descriptorSources.size() + image) = 0u; + } } std::vector visited(program.controlFlow.size()); std::vector pending {0}; + InaccessibleRead conditionRead; + std::uint32_t conditionNullRoots = 0; + cleanEvaluator.ReportInaccessibleReads(&conditionRead); + cleanEvaluator.ReportNullRootReads(&conditionNullRoots); while (!pending.empty()) { const auto index = pending.back(); pending.pop_back(); @@ -84,14 +91,20 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span #include #include +#include #include #include #include @@ -16,20 +17,27 @@ namespace ShaderRecompiler { namespace { -constexpr std::int64_t PatchIndexCount = 256; constexpr std::int64_t LocalVertexIndexCount = 8192; constexpr std::int64_t TriangleFactorBytes = 16; +constexpr std::int64_t QuadFactorBytes = 24; constexpr std::int64_t AddressLimit = 0xffffffffll; struct Affine { std::int64_t vertex = 0; std::int64_t relative = 0; std::int64_t constant = 0; + bool dynamic = false; + std::int64_t dynamicScale = 0; + const IrValue* dynamicRoot = nullptr; - [[nodiscard]] bool Scalar() const { return vertex == 0 && relative == 0; } + [[nodiscard]] bool Scalar() const { return vertex == 0 && relative == 0 && !dynamic; } bool operator==(const Affine& other) const = default; }; +bool Same(const Affine& left, const Affine& right) { + return !left.dynamic && !right.dynamic && left == right; +} + [[noreturn]] void Fail(const char* reason, std::uint32_t pc) { char text[256]; std::snprintf(text, sizeof(text), "tessellation lowering: %s at pc 0x%x", reason, pc); @@ -106,10 +114,12 @@ class AddressEvaluator { public: AddressEvaluator(std::uint32_t vertexCount, std::int64_t relativeCount) : vertexCount(vertexCount), relativeCount(relativeCount) {} - std::optional Evaluate(IrValue* value, IrValue* predicate) { - memo.clear(); + std::optional Evaluate(IrValue* value, IrValue* predicate, bool allowDynamic = false) { + strictMemo.clear(); + dynamicMemo.clear(); + independence.clear(); active = predicate; - return evaluate(value); + return evaluate(value, allowDynamic); } bool IsInvocation(IrValue* value) { @@ -129,16 +139,19 @@ private: [[nodiscard]] std::int64_t lastVertex() const { return vertexCount == 0 ? 0 : static_cast(vertexCount) - 1; } [[nodiscard]] std::optional bounded(const Affine& value) const { + if (value.dynamic) return value; if (Lowest(value) < 0 || Highest(value) > AddressLimit) return std::nullopt; return value; } [[nodiscard]] std::optional scale(const Affine& value, std::int64_t factor) const { - if (factor < 0 || factor >= (1ll << 31)) return std::nullopt; - return bounded({value.vertex * factor, value.relative * factor, value.constant * factor}); + if (factor < 0 || factor >= (1ll << 31) || std::abs(value.dynamicScale) * factor > AddressLimit) return std::nullopt; + if (value.dynamic && factor == 0) return Affine{}; + return bounded({value.vertex * factor, value.relative * factor, value.constant * factor, value.dynamic, value.dynamicScale * factor, value.dynamicRoot}); } [[nodiscard]] std::optional> split(const Affine& value, std::int64_t divisor) const { + if (value.dynamic) return std::nullopt; Affine high; Affine low; (value.vertex % divisor == 0 ? high.vertex : low.vertex) = value.vertex; @@ -158,6 +171,7 @@ private: } [[nodiscard]] std::optional mask(const Affine& value, std::int64_t bits) const { + if (value.dynamic) return std::nullopt; if (value.Scalar()) return Affine{0, 0, value.constant & bits}; const auto width = std::bit_width(static_cast(bits)); if (width >= 32) return bits == AddressLimit ? std::optional(value) : std::nullopt; @@ -169,27 +183,61 @@ private: } [[nodiscard]] std::optional disjoint(const Affine& high, const Affine& low) const { - if (Lowest(low) < 0) return std::nullopt; + if (high.dynamic || low.dynamic || Lowest(low) < 0) return std::nullopt; const auto divisor = 1ll << std::bit_width(static_cast(Highest(low))); if (high.vertex % divisor != 0 || high.relative % divisor != 0 || high.constant % divisor != 0) return std::nullopt; return bounded({high.vertex + low.vertex, high.relative + low.relative, high.constant + low.constant}); } - std::optional evaluate(IrValue* raw) { + bool independentOfRelative(IrValue* raw) { + IrValue* start = raw->Resolve(); + if (const auto found = independence.find(start); found != independence.end()) return found->second; + std::vector pending{start}; + std::unordered_set visited; + bool independent = true; + while (independent && !pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + if (value == nullptr || value->HasImmediate() || !visited.insert(value).second) continue; + if (IsBase(value, TessellationBaseKind::RelativeIndex)) { + independent = false; + break; + } + if (value != start) { + if (const auto known = evaluate(value, false)) { + independent = known->relative == 0; + continue; + } + } + if (active != nullptr && value->Opcode() == IrOpcode::SelectU32 && Implies(active, value->Argument(0))) { + pending.push_back(value->Argument(1)->Resolve()); + continue; + } + for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { + if (IrValue* argument = value->Argument(index)) pending.push_back(argument->Resolve()); + } + } + independence[start] = independent; + return independent; + } + + std::optional evaluate(IrValue* raw, bool dynamic) { IrValue* value = raw->Resolve(); if (value->HasImmediate()) { if (value->Type() != IrType::U32) return std::nullopt; return Affine{0, 0, value->ImmediateU32()}; } + auto& memo = dynamic ? dynamicMemo : strictMemo; if (const auto found = memo.find(value); found != memo.end()) return found->second; memo.emplace(value, std::nullopt); - const auto result = compute(value); + auto result = compute(value, dynamic); + if (!result && dynamic && independentOfRelative(value)) result = Affine{0, 0, 0, true, 1, value}; memo[value] = result; return result; } - std::optional compute(IrValue* value) { - const auto argument = [&](std::size_t index) { return evaluate(value->Argument(index)); }; + std::optional compute(IrValue* value, bool dynamic) { + const auto argument = [&](std::size_t index) { return evaluate(value->Argument(index), dynamic); }; const auto scalar = [&](std::size_t index) -> std::optional { const auto operand = argument(index); if (!operand || !operand->Scalar()) return std::nullopt; @@ -206,7 +254,21 @@ private: const auto rhs = argument(1); if (!lhs || !rhs) return std::nullopt; const std::int64_t sign = value->Opcode() == IrOpcode::IAdd32 ? 1 : -1; - return bounded({lhs->vertex + sign * rhs->vertex, lhs->relative + sign * rhs->relative, lhs->constant + sign * rhs->constant}); + Affine sum{lhs->vertex + sign * rhs->vertex, lhs->relative + sign * rhs->relative, lhs->constant + sign * rhs->constant, lhs->dynamic || rhs->dynamic, lhs->dynamicScale, lhs->dynamicRoot}; + if (rhs->dynamic && !lhs->dynamic) { + sum.dynamicScale = sign * rhs->dynamicScale; + sum.dynamicRoot = rhs->dynamicRoot; + } else if (rhs->dynamic && lhs->dynamicRoot != nullptr && lhs->dynamicRoot == rhs->dynamicRoot) { + sum.dynamicScale = lhs->dynamicScale + sign * rhs->dynamicScale; + if (sum.dynamicScale == 0) { + sum.dynamic = false; + sum.dynamicRoot = nullptr; + } + } else if (rhs->dynamic) { + sum.dynamicScale = std::gcd(lhs->dynamicScale, rhs->dynamicScale); + sum.dynamicRoot = nullptr; + } + return bounded(sum); } case IrOpcode::IMul32: { const auto lhs = argument(0); @@ -257,7 +319,7 @@ private: if (active != nullptr && Implies(active, value->Argument(0))) return argument(1); const auto lhs = argument(1); const auto rhs = argument(2); - return lhs && rhs && *lhs == *rhs ? lhs : std::nullopt; + return lhs && rhs && Same(*lhs, *rhs) ? lhs : std::nullopt; } default: break; @@ -266,15 +328,17 @@ private: const auto first = argument(0); for (std::size_t index = 1; first && index < value->ArgumentCount(); ++index) { const auto other = argument(index); - if (!other || !(*other == *first)) return std::nullopt; + if (!other || !Same(*other, *first)) return std::nullopt; } - return first; + return first && !first->dynamic ? first : std::nullopt; } std::uint32_t vertexCount; std::int64_t relativeCount; IrValue* active = nullptr; - std::unordered_map> memo; + std::unordered_map> strictMemo; + std::unordered_map> dynamicMemo; + std::unordered_map independence; }; bool FirstInvocationOnly(IrValue* raw, AddressEvaluator& evaluator, std::unordered_set& path) { @@ -319,6 +383,8 @@ struct Access { Affine address; IrValue* predicate = nullptr; std::uint32_t pc = 0; + IrValue* addressValue = nullptr; + std::uint32_t memoryOffset = 0; }; std::uint32_t ComponentCount(IrOpcode opcode) { @@ -351,7 +417,7 @@ std::uint32_t ComponentCount(IrOpcode opcode) { class Lowering { public: Lowering(IrProgram& program, const ShaderTessellationInputInfo& tessellation) - : program(program), tessellation(tessellation), stage(program.Resources().stage), evaluator(stage == IrShaderStage::TessellationControl ? tessellation.outputControlPoints : 0u, stage == IrShaderStage::Local ? LocalVertexIndexCount : PatchIndexCount) {} + : 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()) { @@ -383,6 +449,7 @@ public: deadCodeEliminator.Eliminate(program); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; + program.Metadata().tessellationPatchStride = patchStride; } private: @@ -425,10 +492,12 @@ private: address = memory.offen ? inst->Argument(2) : nullptr; } if (address != nullptr) { - const auto evaluated = evaluator.Evaluate(address, access.predicate); + const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput); if (!evaluated) Fail("the tessellation ring or LDS address is not affine in the control point and relative patch", flags.pc); access.address = *evaluated; + access.addressValue = address; } + access.memoryOffset = memory.offset; access.address.constant += memory.offset; if ((access.address.constant & 3) != 0) Fail("unaligned tessellation ring or LDS address", flags.pc); accesses.push_back(access); @@ -444,18 +513,29 @@ private: void derive() { const auto inputs = static_cast(tessellation.inputControlPoints); const auto outputs = static_cast(tessellation.outputControlPoints); - for (const auto& access : accesses) { + 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::ControlOutput && access.address.vertex != 0) agree(controlStride, access.address.vertex, access.pc); if (access.kind == TessellationAttribute::EvaluationInput) { - if (access.address.relative <= 0 || access.address.relative % outputs != 0) Fail("domain shader reads of patch constants or other patches are unsupported", access.pc); - agree(controlStride, access.address.relative / outputs, access.pc); + const auto end = access.address.constant + 4 * static_cast(access.components); + if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { + agree(controlStride, access.address.relative / outputs, access.pc); + } else { + access.kind = TessellationAttribute::PatchInput; + } } } - std::optional patchStride; + std::optional recordStride; std::int64_t patchLowest = AddressLimit; std::int64_t patchHighest = 0; + const auto record = [&](const Access& access, std::int64_t end) { + if (recordStride && *recordStride != access.address.relative) Fail("inconsistent patch-constant stride", access.pc); + recordStride = access.address.relative; + patchLowest = std::min(patchLowest, access.address.constant); + patchHighest = std::max(patchHighest, end); + }; for (auto& access : accesses) { const auto& address = access.address; const auto end = address.constant + 4 * static_cast(access.components); @@ -466,14 +546,12 @@ private: case TessellationAttribute::ControlInput: if (localStride == 0u) Fail("the hull shader reads no LDS per control point, so the local stride is unknown", access.pc); if ((address.vertex != 0 && address.vertex != localStride) || address.relative != inputs * localStride || address.constant < 0 || end + address.vertex * (outputs - 1) > inputs * localStride) Fail("the hull shader reads LDS outside its patch", access.pc); + 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 (controlStride == 0u || address.constant < outputs * controlStride) Fail("the hull shader writes off-chip memory outside its control points and patch constants", access.pc); - if (patchStride && *patchStride != address.relative) Fail("inconsistent patch-constant stride", access.pc); - patchStride = address.relative; - patchLowest = std::min(patchLowest, address.constant); - patchHighest = std::max(patchHighest, end); + 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; requireFirstInvocation(access); } else if (address.relative != outputs * controlStride || address.constant < 0 || end > controlStride) { @@ -483,15 +561,33 @@ private: case TessellationAttribute::EvaluationInput: if (address.constant < 0 || end > outputs * controlStride) Fail("the domain shader reads off-chip memory outside its patch's control points", access.pc); break; - case TessellationAttribute::Factor: - if (tessellation.domain != 1u || address.vertex != 0 || address.relative != TriangleFactorBytes || address.constant < 0 || end > TriangleFactorBytes) Fail("the hull shader writes tessellation factors outside its patch's triangle factors", access.pc); + case TessellationAttribute::Factor: { + const bool quads = tessellation.domain == 2u; + const auto factorBytes = quads ? QuadFactorBytes : TriangleFactorBytes; + if ((tessellation.domain != 1u && !quads) || address.vertex != 0 || address.relative != factorBytes || address.constant < 0 || end > factorBytes) Fail(quads ? "the hull shader writes tessellation factors outside its patch's quad factors" : "the hull shader writes tessellation factors outside its patch's triangle factors", access.pc); requireFirstInvocation(access); break; + } + case TessellationAttribute::PatchInput: + if (controlStride == 0u) Fail("the domain shader reads patch constants but no control point, so their locations are unknown", access.pc); + if (address.relative == 0) Fail("the domain shader reads off-chip memory that is not indexed by its relative patch", access.pc); + if (evaluator.Lowest(address) < static_cast(tessellation.patchesPerGroup) * outputs * controlStride) Fail("the domain shader reads off-chip memory that is neither its patch's control points nor a patch-constant record", access.pc); + record(access, end); + break; case TessellationAttribute::PatchOutput: break; } } - if (patchStride && (*patchStride == 0 || std::abs(*patchStride) < patchHighest - patchLowest)) Fail("patch-constant records of neighbouring patches overlap", 0u); + if (!recordStride) return; + if (*recordStride == 0 || std::abs(*recordStride) < patchHighest - patchLowest) Fail(stage == IrShaderStage::TessellationEvaluation ? "the domain shader reads more than one patch-constant record" : "patch-constant records of neighbouring patches overlap", 0u); + patchStride = static_cast(std::abs(*recordStride)); + } + + bool dynamicInputAddress(IrValue* user, IrValue* value) { + if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; + if (!IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput))) return false; + const auto address = evaluator.Evaluate(value, user->Argument(2), true); + return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; } void requireRelativeIndependence(const std::vector& bases) { @@ -518,6 +614,7 @@ private: default: break; } + 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); } @@ -543,7 +640,10 @@ private: } IrValue& componentAddress(const Access& access, std::uint32_t component) { - IrValue& offset = constant(static_cast(access.address.constant) + 4u * component); + if (access.address.dynamic) return insert(access, IrOpcode::IAdd32, IrType::U32, {access.addressValue, &constant(access.memoryOffset + 4u * component)}); + auto address = access.address.constant + 4 * static_cast(component); + if (access.kind == TessellationAttribute::PatchOutput || access.kind == TessellationAttribute::PatchInput) address -= FloorDiv(address, patchStride) * patchStride; + IrValue& offset = constant(static_cast(address)); if (access.kind != TessellationAttribute::ControlInput || access.address.vertex == 0) return offset; IrValue& invocation = insert(access, IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InvocationId)), &constant(0u)}); IrValue& scaled = insert(access, IrOpcode::IMul32, IrType::U32, {&invocation, &constant(static_cast(access.address.vertex))}); @@ -608,6 +708,7 @@ private: std::vector accesses; std::uint32_t localStride = 0; std::uint32_t controlStride = 0; + std::uint32_t patchStride = 0; }; } diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake new file mode 100644 index 0000000000..2a6e17df0e --- /dev/null +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -0,0 +1,52 @@ +get_target_property(spirvToolsSourceDir SPIRV-Tools-opt SOURCE_DIR) +set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") +file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) + +set(spirvToolsOldTraversal [=[ + for (DominatorTreeNode* child : todo[next_node].node->children_) { + todo.push_back({child, todo[next_node].value_to_id_map}); + } +]=]) +set(spirvToolsNewTraversal [=[ + const auto& children = todo[next_node].node->children_; + for (size_t child_index = 0; child_index < children.size(); ++child_index) { + if (child_index + 1 == children.size()) { + todo.push_back({children[child_index], + std::move(todo[next_node].value_to_id_map)}); + } else { + todo.push_back({children[child_index], todo[next_node].value_to_id_map}); + } + } + todo[next_node].value_to_id_map.clear(); +]=]) +string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) +if(spirvToolsTraversalPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") +endif() +string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") +string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" + "#include \"source/opt/redundancy_elimination.h\"\n\n#include \n" + spirvToolsRedundancy "${spirvToolsRedundancy}") + +set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") +set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") +file(MAKE_DIRECTORY "${spirvToolsPatchedDir}") +file(CONFIGURE OUTPUT "${spirvToolsPatchedSource}" CONTENT "${spirvToolsRedundancy}" @ONLY) + +get_target_property(spirvToolsSources SPIRV-Tools-opt SOURCES) +set(spirvToolsPatchedSources) +set(spirvToolsReplacements 0) +foreach(source IN LISTS spirvToolsSources) + get_filename_component(sourcePath "${source}" ABSOLUTE BASE_DIR "${spirvToolsSourceDir}") + if(sourcePath STREQUAL spirvToolsRedundancySource) + list(APPEND spirvToolsPatchedSources "${spirvToolsPatchedSource}") + math(EXPR spirvToolsReplacements "${spirvToolsReplacements} + 1") + else() + list(APPEND spirvToolsPatchedSources "${source}") + endif() +endforeach() +if(NOT spirvToolsReplacements EQUAL 1) + message(FATAL_ERROR "SPIRV-Tools optimizer sources have changed; review the local memory fix") +endif() +set_property(TARGET SPIRV-Tools-opt PROPERTY SOURCES "${spirvToolsPatchedSources}") diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index 878b0a74dd..44e1e996fe 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -26,6 +26,8 @@ inline constexpr std::uint32_t PushDataOffset = 8188u; inline constexpr std::uint32_t ExportBase = 8192u; inline constexpr std::uint32_t ExportPackingBase = 8224u; inline constexpr std::uint32_t DualSourceBlend = 8232u; +inline constexpr std::uint32_t RasterPrimitive = 8233u; +inline constexpr std::uint32_t LayerExported = 8234u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; @@ -33,6 +35,7 @@ inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t ImageModeBase = 32768u; inline constexpr std::uint32_t ImageModeStride = 256u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; +inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 8cbadf9cce..c0c29d52f8 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}, @@ -1932,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode)) { return true; } switch (opcode) { @@ -1946,7 +1949,6 @@ bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { case RdnaOpcode::VMacF32: case RdnaOpcode::VMadF32: case RdnaOpcode::VFmaF32: - case RdnaOpcode::VFmaF16: case RdnaOpcode::VAddF16: case RdnaOpcode::VSubF16: case RdnaOpcode::VSubrevF16: diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index df79d3e838..aacd8989dd 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -237,6 +237,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat } deadCodeEliminator.Eliminate(program); } + if (constantFolder.ComposePackedAncillary(program)) { + deadCodeEliminator.Eliminate(program); + } }; simplify(); if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) { @@ -315,14 +318,27 @@ public: }; struct ResourceProgram { - explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) : program(std::make_unique(PrepareResourceProgram(request, preparation))), plan(std::make_shared(ResourceMaterializer{}.ExtractPlan(*program))) {} + explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) : program(std::make_unique(PrepareResourceProgram(request, preparation))) { + const auto clearRegisterState = [](const auto& storage) { + for (const auto& block : storage) { + block->ssaScalarValues.fill(nullptr); + block->ssaVectorValues.fill(nullptr); + block->ssaThreadBitScalarValues.fill(nullptr); + block->ssaScalarMaskTags.fill(nullptr); + } + }; + clearRegisterState(program->Blocks()); + clearRegisterState(program->Resources().blockStorage); + if (reclaim) program->ReclaimUnusedValues(); + plan = std::make_shared(ResourceMaterializer{}.ExtractPlan(*program)); + } std::unique_ptr program; std::shared_ptr plan; }; std::shared_ptr makeResourcePlan(const RecompileRequest& request) { - return ResourceProgram(request).plan; + return ResourceProgram(request, nullptr, false).plan; } struct SourceKeyHash { @@ -341,7 +357,7 @@ bool FailureMemo() { return memo; } -std::shared_ptr getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr) { +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. static std::unordered_map, std::vector>, SourceKeyHash> sources; @@ -385,7 +401,7 @@ std::shared_ptr getSource(const RecompileRequest& request, ShaderPr if (source->plan == nullptr) { if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); try { - ResourceProgram resource(request, preparation); + ResourceProgram resource(request, preparation, reclaim); source->plan = std::move(resource.plan); source->program = std::move(resource.program); } catch (...) { @@ -435,6 +451,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra targetOptions.float64DenormPreserve = request.target.float64DenormPreserve; targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; + targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; @@ -635,7 +652,7 @@ struct RegistrationModuleEntry { std::shared_ptr registrationModule(std::uint64_t variantId, std::span moduleKey, const std::function()>& specialize) { std::vector key{variantId}; key.insert(key.end(), moduleKey.begin(), moduleKey.end()); - static std::shared_mutex mutex; + static PrimedSharedMutex mutex; static std::map, std::shared_ptr> modules; std::shared_ptr entry; { @@ -772,6 +789,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast(packing)); moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u); + if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast(request.context.pixel->rasterPrimitive)); + if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u); } result.vertexAttributes.reserve(result.vertexInputs.size()); std::array vertexClasses{}; @@ -818,6 +837,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil if (request.context.pixel) { for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]}); constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]}); + if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]}); + if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]}); } if (!artifact.vertexInputPatches.empty()) { for (const auto& input : result.vertexInputs) { @@ -1018,6 +1039,8 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto mapping : request.context.pixel->targetExportMapping) mix(mapping); for (const auto packing : request.context.pixel->targetExportPacking) mix(static_cast(packing)); mix(request.context.pixel->dualSourceBlend); + mix(static_cast(request.context.pixel->rasterPrimitive)); + mix(request.context.pixel->layerExported); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; @@ -1180,7 +1203,7 @@ std::shared_ptr PrepareShader(const RecompileRequest& reques return recompileReporting(request, [&]() -> std::shared_ptr { static_cast(RequestInputInfo(request)); auto handle = std::make_shared(); - handle->source = getSource(request, preparation); + handle->source = getSource(request, preparation, false); RecompileCacheKey::BuildInterface(request, handle->staticKey); handle->staticKey.push_back(HostSubgroupSize(request)); bool cacheHit = false; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index eb84c86ddc..9a22de5259 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -113,6 +113,13 @@ enum class ColorExportPacking : std::uint8_t { Unorm10_11_11 }; +enum class RasterPrimitive : std::uint8_t { + Point, + Line, + Triangle, + Rectangle +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -142,6 +149,8 @@ struct ShaderPixelStageInfo { bool dualSourceBlend; bool alphaToCoverage; bool stencilRefExportEnable; + RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle; + bool layerExported = false; }; struct ShaderVertexBufferResource { @@ -163,6 +172,7 @@ struct ShaderVertexStageInfo { std::uint32_t fetchAttribReg; std::uint32_t fetchBufferReg; bool fetchEmbedded; + std::uint32_t paClVsOutCntl = 0; }; struct ShaderFloatMode { @@ -233,6 +243,7 @@ struct SpirvTarget { bool registrationModules = false; std::uint32_t registrationWordLimit = 200000u; bool vertexSubgroups = true; + bool float16DenormPreserve = false; }; struct BindingLayout { @@ -277,10 +288,13 @@ struct TessellationConfiguration { std::uint32_t domain; std::uint32_t partitioning; std::uint32_t outputTopology; + std::uint32_t patchesPerGroup = 256; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; inline constexpr std::uint32_t MeshDrawPushBytes = 24; +inline constexpr std::uint32_t TessellationLevelPushOffsetBytes = 120; +inline constexpr std::uint32_t TessellationLevelPushBytes = 8; inline constexpr std::uint32_t MeshArgumentAddressDword = 4; inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3; inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 1d85b66683..f25dc30969 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -11,6 +11,8 @@ #define WIN32_LEAN_AND_MEAN #endif #include +#elif defined(__APPLE__) +#include #endif #include "ShaderCacheVersion.hpp" #include @@ -53,7 +55,15 @@ std::filesystem::path ShaderCacheDirectory() { const char* directory = std::getenv("ANYPS5_SHADER_CACHE_DIR"); if (directory != nullptr && *directory != '\0') return std::filesystem::path(directory); std::error_code error; +#ifdef __APPLE__ + std::uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + std::vector buffer(size); + if (_NSGetExecutablePath(buffer.data(), &size) != 0) return {}; + const auto executable = std::filesystem::canonical(buffer.data(), error); +#else const auto executable = std::filesystem::read_symlink("/proc/self/exe", error); +#endif if (error) return {}; return executable.parent_path() / "shader_cache"; #endif @@ -546,6 +556,8 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { writer.Value(info.bdaWrites); writer.Value(info.usesFaultBuffer); writer.Value(info.dispatchThreadLimit); + writer.Value(info.rasterPrimitive); + writer.Value(info.layerExported); encodeLayout(writer, compiled.bindings); } @@ -655,6 +667,8 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { reader.Value(info.bdaWrites); reader.Value(info.usesFaultBuffer); reader.Value(info.dispatchThreadLimit); + reader.Value(info.rasterPrimitive); + reader.Value(info.layerExported); decodeLayout(reader, compiled.bindings); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 9747d875b6..81bbd87beb 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 27; +inline constexpr std::uint32_t FormatVersion = 29; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 65cb02117a..9d22da5885 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -23,6 +23,7 @@ struct SpirvTargetOptions { bool float64DenormPreserve = false; bool registrationModules = false; bool fragmentDescriptorSet = false; + bool float16DenormPreserve = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index eceb3b3d61..bbc32567d2 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -16,6 +16,7 @@ const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimen std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components); +std::uint32_t TypeU8(SpirvEmitterState& state); std::uint32_t TypeU32(SpirvEmitterState& state); std::uint32_t TypeU64(SpirvEmitterState& state); std::uint32_t TypeScalarU64(SpirvEmitterState& state); @@ -33,6 +34,9 @@ std::uint32_t TypeStorageBufferPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferElementPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64Pointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state); std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state); std::uint32_t TypePushConstantElementPointer(SpirvEmitterState& state); std::uint32_t TypeU32ArrayPointer(SpirvEmitterState& state, std::uint32_t storageClass, std::uint32_t dwords); @@ -131,9 +135,11 @@ F32Class EmitClassifyF32(SpirvEmitterState& state, std::uint32_t value); std::uint32_t EmitClassMaskBitMatch(SpirvEmitterState& state, std::uint32_t mask, std::uint32_t bit, std::uint32_t classMatch); std::uint32_t EmitClassMaskF32(SpirvEmitterState& state, std::uint32_t value, std::uint32_t mask); std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs, bool maxValue); -std::uint32_t EmitFlushF32DenormToSignedZero(SpirvEmitterState& state, std::uint32_t value); std::uint32_t EmitTrigCycleF32(SpirvEmitterState& state, std::uint32_t src, bool preserveSignedZero); +void EmitF16DenormPreserve(SpirvEmitterState& state); std::uint32_t EmitF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitF16BitsToF32AnyNan(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bits); void EmitProgram(SpirvEmitterState& state); void DefineGetBdaPointer(SpirvEmitterState& state); void DefineDot2F32F16Function(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 3c5550ac8d..0ad1aefdb9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -211,7 +211,7 @@ std::uint32_t EmitBitCastF16U16(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertU32U16(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertU32U8(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitBitCastU32F32(SpirvEmitterState& state, std::uint32_t arg0); -std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, std::uint32_t bits); +std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, const IrValue& inst, std::uint32_t bits); std::uint32_t EmitCompositeExtractU32x2(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitCompositeExtractU32x3(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitCompositeExtractU32x4(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index a26c20a548..f5d6a95d9e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -81,9 +81,8 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; - std::uint32_t tessPatchBase = 0; SpirvRequirements requirements; std::uint32_t laneCount = 1; @@ -93,6 +92,7 @@ struct SpirvEmitterState { bool float64DenormPreserve = false; bool registrationModules = false; bool fragmentDescriptorSet = false; + bool float16DenormPreserve = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; // The target's SPIR-V version and what the device accepts, for capabilities an emitter adds @@ -107,6 +107,7 @@ struct SpirvEmitterState { std::uint32_t storageBufferVariable = 0; std::uint32_t storageBufferU64Variable = 0; std::uint32_t storageBufferCoherentVariable = 0; + std::uint32_t storageBufferU8Variable = 0; std::vector memoryByteOffsets; std::map, std::uint32_t> formattedBufferFunctions; std::map, std::uint32_t> formattedGpuBufferFunctions; @@ -123,6 +124,7 @@ struct SpirvEmitterState { std::array bdaByteWriteFunctions {}; std::uint32_t bdaFaultFunction = 0; bool nativeF16ModesEmitted = false; + bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp index c1fb1ae9c0..bf9ea5769c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvTypes.hpp @@ -9,6 +9,7 @@ namespace ShaderRecompiler { std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components); +std::uint32_t TypeU8(SpirvEmitterState& state); std::uint32_t TypeU32(SpirvEmitterState& state); std::uint32_t TypeU64(SpirvEmitterState& state); std::uint32_t TypeScalarU64(SpirvEmitterState& state); @@ -26,6 +27,9 @@ std::uint32_t TypeStorageBufferPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferElementPointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64Pointer(SpirvEmitterState& state); std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state); +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state); std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state); std::uint32_t TypePushConstantElementPointer(SpirvEmitterState& state); std::uint32_t TypeU32ArrayPointer(SpirvEmitterState& state, std::uint32_t storageClass, std::uint32_t dwords); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 9936d3199b..69ff59f2f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -842,8 +842,7 @@ std::uint32_t EmitFPMedTri32(SpirvEmitterState& state, std::uint32_t arg0, std:: } std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { - const auto source = EmitFlushF32DenormToSignedZero(state, arg0); - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), source); + return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); } std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -851,15 +850,15 @@ std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitFPRecipSqrt32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {arg0}); } std::uint32_t EmitFPSqrt(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450Sqrt, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450Sqrt, {arg0}); } std::uint32_t EmitFPExp2(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {EmitFlushF32DenormToSignedZero(state, arg0)}); + return EmitExt(state, TypeF32(state), GLSLstd450Exp2, {arg0}); } std::uint32_t Log2SeriesNearOne(SpirvEmitterState& state, std::uint32_t difference) { @@ -872,9 +871,8 @@ std::uint32_t Log2SeriesNearOne(SpirvEmitterState& state, std::uint32_t differen } std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { - const auto source = EmitFlushF32DenormToSignedZero(state, arg0); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Log2, {source}); - const auto difference = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), source, ConstantF32(state, 0x3f800000u))); + const auto value = EmitExt(state, TypeF32(state), GLSLstd450Log2, {arg0}); + const auto difference = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), arg0, ConstantF32(state, 0x3f800000u))); const auto series = Log2SeriesNearOne(state, difference); const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); @@ -1412,7 +1410,7 @@ static void EmitNativeF16Modes(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityDenormPreserve); if (!state.nativeF16ModesEmitted) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeRoundingModeRTE, 16u); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 16u); + EmitF16DenormPreserve(state); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 16u); state.nativeF16ModesEmitted = true; } @@ -1544,8 +1542,44 @@ std::uint32_t EmitBitCastU32F32(SpirvEmitterState& state, std::uint32_t arg0) { return EmitBitCastU16F16(state, arg0); } -std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, std::uint32_t bits) { - return EmitF16BitsToF32(state, bits); +static bool NarrowF16Field(const IrValue& half) { + const IrValue* value = half.Resolve(); + while (value->Opcode() == IrOpcode::BitCastF16U16 || value->Opcode() == IrOpcode::ConvertU16U32) value = value->Argument(0)->Resolve(); + if (value->Opcode() != IrOpcode::BitFieldUExtract && value->Opcode() != IrOpcode::BitFieldSExtract) return false; + const IrValue* count = value->Argument(2)->Resolve(); + return !count->HasImmediate() || count->ImmediateU32() < 16u; +} + +static bool OnlyCompared(const IrValue& value) { + return std::ranges::all_of(value.Uses(), [](const IrValue* user) { + switch (user->Opcode()) { + 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: + return true; + case IrOpcode::FPAbs32: + case IrOpcode::FPNeg32: + case IrOpcode::Identity: + return OnlyCompared(*user); + default: + return false; + } + }); +} + +std::uint32_t EmitConvertF32F16(SpirvEmitterState& state, const IrValue& inst, std::uint32_t bits) { + if (NarrowF16Field(*inst.Argument(0))) return EmitF16BitsToF32Decoded(state, bits); + return OnlyCompared(inst) ? EmitF16BitsToF32AnyNan(state, bits) : EmitF16BitsToF32(state, bits); } std::uint32_t EmitCompositeExtractU32x2(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 5f9f553014..663a001ba7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -127,7 +127,41 @@ std::uint32_t BdaSpanReadResultType(SpirvEmitterState& state) { return state.bdaStopsInvocations ? state.module.Type(spv::OpTypeStruct, values, TypeU32(state)) : values; } -std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent) { +std::uint32_t DefineBdaSpanLoadFunction(SpirvEmitterState& state, bool coherent) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto values = TypeU32Vector(state, 4); + const auto function = state.module.AllocateId(); + state.module.AddName(function, std::string("load_bda_span") + (coherent ? "_coherent" : "")); + state.module.AddFunction(spv::OpFunction, values, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, values, u64, u32, u32)); + const auto physical = state.module.AllocateId(); + const auto firstBytes = state.module.AllocateId(); + const auto used = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u64, physical); + state.module.AddFunction(spv::OpFunctionParameter, u32, firstBytes); + state.module.AddFunction(spv::OpFunctionParameter, u32, used); + EmitLabel(state, state.module.AllocateId()); + std::array loaded{}; + for (std::uint32_t dword = 0; dword < 4u; ++dword) { + const auto isUsed = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, used, ConstantU32(state, 1u << dword)), ConstantU32(state, 0u)); + loaded[dword] = EmitValueOrZeroIfCondition(state, isUsed, [&] { + const auto delta = Unary(state, spv::OpUConvert, u64, Binary(state, spv::OpISub, u32, ConstantU32(state, dword * 4u), firstBytes)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, Binary(state, spv::OpIAdd, u64, physical, delta)); + const auto value = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, value, pointer, BdaAccessMask(coherent), 4u); + return value; + }); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, values, result, loaded[0], loaded[1], loaded[2], loaded[3]); + state.module.AddFunction(spv::OpReturnValue, result); + state.module.AddFunction(spv::OpFunctionEnd); + return function; +} + +std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent, std::uint32_t loader) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -166,19 +200,9 @@ std::uint32_t DefineBdaSpanReadFunction(SpirvEmitterState& state, bool coherent) return value; }; const auto read = EmitValueIfElse(state, wide, values, [&] { - std::array loaded{}; - for (std::uint32_t dword = 0; dword < 4u; ++dword) { - loaded[dword] = EmitValueOrZeroIfCondition(state, isUsed(dword), [&] { - const auto delta = Unary(state, spv::OpUConvert, u64, Binary(state, spv::OpISub, u32, ConstantU32(state, dword * 4u), firstBytes)); - const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, Binary(state, spv::OpIAdd, u64, physical, delta)); - const auto value = state.module.AllocateId(); - if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); - else state.module.AddFunction(spv::OpLoad, u32, value, pointer, BdaAccessMask(coherent), 4u); - return value; - }); - } - return composite(loaded); + const auto loaded = state.module.AllocateId(); + 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) { @@ -281,13 +305,17 @@ void DefineBdaSpanReadFunctions(SpirvEmitterState& state) { const bool coherentAccesses = std::any_of(memory.begin(), memory.end(), [](const MemoryInfo& info) { return info.coherent; }); const bool stops = state.bdaStopsInvocations; const auto zero = ConstantU32(state, 0u); + std::array loaders{}; + for (const bool coherent : {false, true}) { + if (!coherent || coherentAccesses) loaders[coherent] = DefineBdaSpanLoadFunction(state, coherent); + } for (const bool stop : {false, true}) { if (stop && !stops) continue; state.bdaStopsInvocations = stop; state.bdaStopValue = stop ? state.module.Constant(spv::OpConstantComposite, BdaSpanReadResultType(state), state.module.Constant(spv::OpConstantComposite, TypeU32Vector(state, 4), zero, zero, zero, zero), ConstantU32(state, 1u)) : 0u; for (const bool coherent : {false, true}) { if (coherent && !coherentAccesses) continue; - state.bdaSpanReadFunctions[stop][coherent] = DefineBdaSpanReadFunction(state, coherent); + state.bdaSpanReadFunctions[stop][coherent] = DefineBdaSpanReadFunction(state, coherent, loaders[coherent]); } } state.bdaStopsInvocations = stops; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 3db76e6e7b..2bd877635a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -252,6 +252,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float64DenormPreserve = target.float64DenormPreserve; state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; + state.float16DenormPreserve = target.float16DenormPreserve; state.hostSubgroupSize = target.subgroupSize; if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 1870ca1878..769ec4c329 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -217,6 +217,11 @@ void EmitStructuredTerminator(SpirvValueEmitContext& ctx, const IrProgram& progr const std::string message = "branch condition " + std::string(IrOpcodeName(resolved->Opcode())) + " of block " + std::to_string(info.id) + " (defined in " + where + ") was not emitted before the branch"; ctx.Fail(message.c_str()); } + if (ctx.Label(trueBlock) == ctx.Label(falseBlock)) { + if (term.loopHeader) emitMerge(); + state.module.AddFunction(spv::OpBranch, ctx.Label(trueBlock)); + return; + } auto condition = EmitBranchCondition(ctx, info); if (state.loopGuardLimit != 0 && term.trueBlock != term.falseBlock) { if (IsLoopMerge(program, term.trueBlock)) condition = GuardLoopExit(state, info, condition, true); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index d380b33fc1..93902d26d5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -181,7 +181,7 @@ std::uint32_t CubeLayer(SpirvEmitterState& state, std::uint32_t value) { return result; } -std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded) { +std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t first, std::uint32_t components, std::uint32_t encoded, bool gradient = false) { const bool cube = access.image.cube; const auto present = std::min(encoded, components); if (first == NoImageComponent || (encoded < components && (cube || encoded == 0u)) || access.mem.imageAddressComponents < first + present) { @@ -193,7 +193,7 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access return x; } auto y = component(1u); - if (cube) { + if (cube && !gradient) { x = CubeAxis(ctx.state, x); y = CubeAxis(ctx.state, y); } @@ -1416,8 +1416,8 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con std::vector operands; if (HasFlag(mem, RdnaImageSampleFlagDerivative)) { operandMask |= spv::ImageOperandsGradMask; - operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents)); - operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents)); + operands.push_back(CoordF32(ctx, access, setup.layout.gradX, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true)); + operands.push_back(CoordF32(ctx, access, setup.layout.gradY, setup.dimensionInfo.spatialComponents, AddressDimension(access).spatialComponents, true)); } else if (explicitLod) { operandMask |= spv::ImageOperandsLodMask; operands.push_back(HasFlag(mem, RdnaImageSampleFlagLod) ? AddressF32(ctx, access, setup.layout.lod) : ZeroF32(state)); @@ -1474,10 +1474,10 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operands.insert(operands.end() - ((operandMask & spv::ImageOperandsMinLodMask) != 0u ? 1 : 0), offset); } - const auto emitSample = [&](std::uint32_t coord, std::uint32_t mask, const std::vector& arguments) { + const auto emitSample = [&](std::uint32_t coord, std::uint32_t mask, const std::vector& arguments, std::uint32_t sampleOpcode) { const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); const auto sample = state.module.AllocateId(); - std::vector words = {opcode, resultType, sample, sampled, coord}; + std::vector words = {sampleOpcode, resultType, sample, sampled, coord}; if (setup.dref) { words.push_back(drefValue); } @@ -1489,10 +1489,19 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con return sample; }; std::uint32_t sample = 0; - if (!foldOffset) { - sample = emitSample(setup.coord, operandMask, operands); + const bool baseLevelIfUnnormalized = !setup.dref && SamplesBaseLevelIfUnnormalized(state.program.Info().samplers.at(mem.sampler)); + if (baseLevelIfUnnormalized) { + const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::UnnormalizedBaseLevelBase + mem.sampler, 0u); + const auto unnormalized = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); + sample = EmitValueIfElse(state, unnormalized, resultType, [&] { + return emitSample(setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); + }, [&] { + return emitSample(setup.coord, operandMask, operands, opcode); + }); + } else if (!foldOffset) { + sample = emitSample(setup.coord, operandMask, operands, opcode); } else if (!offsetOperand) { - sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); + sample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); } else { const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::FoldOffsetBase + mem.sampler, 0u); const auto folded = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); @@ -1502,11 +1511,11 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, folded, foldLabel, operandLabel); EmitLabel(state, foldLabel); - const auto foldSample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands); + const auto foldSample = emitSample(FoldedOffsetCoord(ctx, access, setup), foldedMask, foldedOperands, opcode); const auto foldEnd = state.currentLabel; state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, operandLabel); - const auto operandSample = emitSample(setup.coord, operandMask, operands); + const auto operandSample = emitSample(setup.coord, operandMask, operands, opcode); const auto operandEnd = state.currentLabel; state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, merge); @@ -1583,7 +1592,7 @@ void EmitImageOperation(SpirvValueEmitContext& context, const IrValue& value, st throw std::logic_error("EmitImageOperation(SpirvValueEmitContext&) is not used: image instructions are emitted per opcode through EmitImage"); } -void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageResource& image) { +void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageResource& image, const TableSelection& table) { const auto irOpcode = inst.Opcode(); const auto imageInfo = ImageOpcodeInfoOf(irOpcode); const auto& mem = ctx.Memory(inst); @@ -1592,7 +1601,7 @@ void EmitImageMode(SpirvValueEmitContext& ctx, const IrValue& inst, const ImageR if (address == nullptr) { ctx.Fail(inst, "has no image address"); } - const ImageEmitAccess access{inst, mem, image, *address, TableSlot(ctx, inst, mem, image)}; + const ImageEmitAccess access{inst, mem, image, *address, table}; if (imageInfo.access == ImageAccess::Atomic) { EmitAtomicOp(ctx, access); return; @@ -1645,7 +1654,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { } else if (mode.depthBits && memory.dataBits == 16u) { ctx.Fail(inst, "depth bits require a 32-bit image result"); } else { - EmitImageMode(ctx, inst, mode); + EmitImageMode(ctx, inst, mode, table); } }; if (modes.size() == 1u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 30fd199ca2..9f1ca47c70 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -384,7 +384,7 @@ void DefineOutputs(SpirvEmitterState& state) { void DefineDescriptors(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; const IrShaderStage stage = state.program.Resources().stage; - if (layout.UsesPushData() || stage == IrShaderStage::Mesh) { + if (layout.UsesPushData() || stage == IrShaderStage::Mesh || stage == IrShaderStage::TessellationControl) { const auto type = PushConstantBlockType(state); state.pushConstantVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPushConstant, type), spv::StorageClassPushConstant); state.module.AddName(type, "BufferResource"); @@ -436,6 +436,7 @@ void DefineDescriptors(SpirvEmitterState& state) { break; case DescriptorBindingKind::Gds: state.gdsVariable = Define(StorageBufferBlockType(state), "gds"); + state.module.AddAnnotation(spv::OpDecorate, state.gdsVariable, spv::DecorationCoherent); break; default: { if (ImageBindingResourceClass(binding.kind) == ImageResourceClass::None) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index a6ada9eba0..f4c1da3f2b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -106,7 +106,11 @@ void DefineModule(SpirvEmitterState& state) { if (state.cullDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityCullDistance); } - if (state.layerVariable != 0) { + const bool layerExtension = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityShaderViewportIndexLayerEXT)) != state.supportedCapabilities.end(); + if (state.layerVariable != 0 && layerExtension) { + state.module.EmitCapability(spv::CapabilityShaderViewportIndexLayerEXT); + state.module.EmitExtension("SPV_EXT_shader_viewport_index_layer"); + } else if (state.layerVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderLayer); } @@ -193,7 +197,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable && !pixel.psStencilRefExportEnable && !pixel.psAlphaToCoverage) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp index bdc7ced720..1265fd1b02 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp @@ -25,6 +25,14 @@ std::uint32_t StorageBufferU64Type(SpirvEmitterState& state) { array); } +std::uint32_t StorageBufferU8Type(SpirvEmitterState& state) { + const auto array = state.module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 1u}}}, TypeU8(state)); + return state.module.DecoratedType(spv::OpTypeStruct, + {{spv::OpDecorate, {spv::DecorationBlock}}, + {spv::OpMemberDecorate, {0, spv::DecorationOffset, 0}}}, + array); +} + } @@ -40,6 +48,10 @@ std::uint32_t TypeBoolVector(SpirvEmitterState& state, std::uint32_t components) return state.module.Type(spv::OpTypeVector, TypeBool(state), components); } +std::uint32_t TypeU8(SpirvEmitterState& state) { + return state.module.Type(spv::OpTypeInt, 8u, 0u); +} + std::uint32_t TypeU32(SpirvEmitterState& state) { return state.module.Type(spv::OpTypeInt, 32u, 0u); } @@ -113,6 +125,18 @@ std::uint32_t TypeStorageBufferU64ElementPointer(SpirvEmitterState& state) { return TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)); } +std::uint32_t TypeStorageBufferU8Block(SpirvEmitterState& state) { + return StorageBufferU8Type(state); +} + +std::uint32_t TypeStorageBufferU8Pointer(SpirvEmitterState& state) { + return TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU8Type(state)); +} + +std::uint32_t TypeStorageBufferU8ElementPointer(SpirvEmitterState& state) { + return TypePointer(state, spv::StorageClassStorageBuffer, TypeU8(state)); +} + std::uint32_t TypePhysicalU32Pointer(SpirvEmitterState& state) { return TypePointer(state, spv::StorageClassPhysicalStorageBuffer, TypeU32(state)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 236724c004..debd3304b6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -45,6 +45,12 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::DispatchThreadLimit) { return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); } + if (kind == StageInputKind::RasterPrimitive) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::RasterPrimitive, static_cast(RasterPrimitive::Triangle)); + } + if (kind == StageInputKind::LayerExported) { + return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerExported, 0u); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { @@ -502,6 +508,10 @@ std::uint32_t MeshOutputType(SpirvEmitterState& state, StageOutputKind kind) { namespace { +std::uint32_t TessellationOuterFactors(const SpirvEmitterState& state) { + return state.inputInfo.vertex->tess.domain == 2u ? 4u : 3u; +} + std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto kind = static_cast(inst.Argument(0)->ImmediateU32()); @@ -509,7 +519,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; const auto pointer = state.module.AllocateId(); if (kind == TessellationAttribute::Factor) { @@ -517,35 +527,40 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins throw std::runtime_error("tessellation factor index must be immediate"); } const auto index = inst.Argument(1)->ImmediateU32() / 4u; - const bool outer = index < 3u; - if (index >= 4u) { + const auto outerFactors = TessellationOuterFactors(state); + const bool outer = index < outerFactors; + if (index >= outerFactors + (outerFactors == 4u ? 2u : 1u)) { throw std::runtime_error("tessellation factor index out of range"); } - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeF32(state)), pointer, outer ? variable : state.tessInnerVariable, ConstantU32(state, outer ? index : index - 3u)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeF32(state)), pointer, outer ? variable : state.tessInnerVariable, ConstantU32(state, outer ? index : index - outerFactors)); return pointer; } - auto address = ctx.Arg(inst, 1); - if (kind == TessellationAttribute::PatchOutput) { - address = EmitBinaryU32(state, spv::OpISub, address, ConstantU32(state, state.tessPatchBase)); - } + const auto address = ctx.Arg(inst, 1); + const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; const auto type = TypePointer(state, storage, TypeU32(state)); + const auto inPatch = [&](std::uint32_t vertex) { + if (kind != TessellationAttribute::ControlInput) return vertex; + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); + return clamped; + }; const auto* sum = inst.Argument(1); const auto* scaled = sum->Opcode() == IrOpcode::IAdd32 ? sum->Argument(0) : nullptr; if (kind == TessellationAttribute::ControlInput && scaled != nullptr && sum->Argument(1)->HasImmediate() && scaled->Opcode() == IrOpcode::IMul32 && scaled->Argument(1)->HasImmediate() && scaled->Argument(1)->ImmediateU32() == stride) { const auto constant = sum->Argument(1)->ImmediateU32(); - const auto vertex = EmitBinaryU32(state, spv::OpIAdd, ctx.Arg(*scaled, 0), ConstantU32(state, constant / stride)); + 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)); return pointer; } - const auto offset = kind == TessellationAttribute::PatchOutput ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); + const auto offset = patch ? address : EmitBinaryU32(state, spv::OpUMod, address, ConstantU32(state, stride)); const auto attribute = EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 4u)); const auto component = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, offset, ConstantU32(state, 2u)), ConstantU32(state, 3u)); - if (kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::PatchOutput) { + if (kind == TessellationAttribute::LocalOutput || patch) { state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, attribute, component); } else { - const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride)); + const auto vertex = kind == TessellationAttribute::ControlOutput ? EmitLaneId(state) : inPatch(EmitBinaryU32(state, spv::OpUDiv, address, ConstantU32(state, stride))); state.module.AddFunction(spv::OpAccessChain, type, pointer, variable, vertex, attribute, component); } return pointer; @@ -554,23 +569,13 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; - std::uint32_t patchBegin = std::numeric_limits::max(); - std::uint32_t patchEnd = 0u; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { continue; } - const auto kind = inst->Argument(0)->ImmediateU32(); - used.at(kind) = true; - if (kind == static_cast(TessellationAttribute::PatchOutput)) { - if (!inst->Argument(1)->HasImmediate()) { - throw std::runtime_error("tessellation patch output offset must be immediate"); - } - patchBegin = std::min(patchBegin, inst->Argument(1)->ImmediateU32()); - patchEnd = std::max(patchEnd, inst->Argument(1)->ImmediateU32() + 4u); - } + used.at(inst->Argument(0)->ImmediateU32()) = true; } } if (std::none_of(used.begin(), used.end(), [](bool value) { return value; })) { @@ -585,14 +590,14 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; std::uint32_t type; if (kind == TessellationAttribute::Factor) { type = array(TypeF32(state), 4u); - } else if (kind == TessellationAttribute::PatchOutput) { - state.tessPatchBase = patchBegin; - type = array(TypeU32Vector(state, 4u), (patchEnd - patchBegin + 15u) / 16u); + } else if (patch) { + type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { const bool local = kind == TessellationAttribute::LocalOutput || kind == TessellationAttribute::ControlInput; const auto stride = local ? state.program.Metadata().tessellationLocalStride : state.program.Metadata().tessellationControlStride; @@ -609,9 +614,9 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationPatch); } else { - state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, kind == TessellationAttribute::PatchOutput ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, patch ? (state.program.Metadata().tessellationControlStride + 15u) / 16u : 0u); } - if (kind == TessellationAttribute::Factor || kind == TessellationAttribute::PatchOutput) { + if (kind == TessellationAttribute::Factor || patch) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationPatch); } } @@ -619,16 +624,16 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { void DefineTessellationExecutionModes(SpirvEmitterState& state) { const auto& tess = state.inputInfo.vertex->tess; - if (tess.domain != 1u || tess.partitioning != 2u || tess.outputTopology != 2u) { + if ((tess.domain != 1u && tess.domain != 2u) || 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); if (state.program.Resources().stage == IrShaderStage::TessellationControl) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, tess.outputControlPoints); } else { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeTriangles); + state.module.AddExecutionMode(state.mainFunc, tess.domain == 2u ? spv::ExecutionModeQuads : spv::ExecutionModeTriangles); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSpacingFractionalOdd); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeVertexOrderCw); + state.module.AddExecutionMode(state.mainFunc, tess.outputTopology == 3u ? spv::ExecutionModeVertexOrderCcw : spv::ExecutionModeVertexOrderCw); } } @@ -940,26 +945,6 @@ std::uint32_t EmitMinMaxF32Value(SpirvEmitterState& state, std::uint32_t lhs, st return Unary(state, spv::OpBitcast, TypeF32(state), resultBits); } -std::uint32_t EmitFlushF32DenormToSignedZero(SpirvEmitterState& state, std::uint32_t value) { - const auto bits = state.module.AllocateId(); - const auto absBits = state.module.AllocateId(); - const auto signBits = state.module.AllocateId(); - const auto nonZero = state.module.AllocateId(); - const auto subnormal = state.module.AllocateId(); - const auto flush = state.module.AllocateId(); - const auto selected = state.module.AllocateId(); - const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); - state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), absBits, bits, ConstantU32(state, 0x7fffffffu)); - state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), signBits, bits, ConstantU32(state, 0x80000000u)); - state.module.AddFunction(spv::OpINotEqual, TypeBool(state), nonZero, absBits, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpULessThan, TypeBool(state), subnormal, absBits, ConstantU32(state, 0x00800000u)); - state.module.AddFunction(spv::OpLogicalAnd, TypeBool(state), flush, nonZero, subnormal); - state.module.AddFunction(spv::OpSelect, TypeU32(state), selected, flush, signBits, bits); - state.module.AddFunction(spv::OpBitcast, TypeF32(state), result, selected); - return result; -} - std::uint32_t EmitTrigCycleF32(SpirvEmitterState& state, std::uint32_t src, bool preserveSignedZero) { const auto rounded = state.module.AllocateId(); const auto reduced = state.module.AllocateId(); @@ -979,7 +964,37 @@ std::uint32_t EmitTrigCycleF32(SpirvEmitterState& state, std::uint32_t src, bool return result; } +void EmitF16DenormPreserve(SpirvEmitterState& state) { + if (state.f16DenormPreserveEmitted) return; + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 16u); + state.f16DenormPreserveEmitted = true; +} + +static std::uint32_t UnpackF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits, bool nanBits) { + if (!state.float16DenormPreserve) return EmitF16BitsToF32Decoded(state, bits); + EmitF16DenormPreserve(state); + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto unpacked = state.module.AllocateId(); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32Vector(state, 2), unpacked, GlslStd450(state), GLSLstd450UnpackHalf2x16, bits); + state.module.AddFunction(spv::OpCompositeExtract, f32, value, unpacked, 0u); + if (!nanBits) return value; + const auto signAndPayload = Binary(state, spv::OpShiftRightArithmetic, u32, Binary(state, spv::OpShiftLeftLogical, u32, bits, ConstantU32(state, 16u)), ConstantU32(state, 3u)); + const auto nan = Unary(state, spv::OpBitcast, f32, Binary(state, spv::OpBitwiseOr, u32, signAndPayload, ConstantU32(state, 0x7f800000u))); + return Select(state, f32, Unary(state, spv::OpIsNan, TypeBool(state), value), nan, value); +} + std::uint32_t EmitF16BitsToF32(SpirvEmitterState& state, std::uint32_t bits) { + return UnpackF16BitsToF32(state, bits, true); +} + +std::uint32_t EmitF16BitsToF32AnyNan(SpirvEmitterState& state, std::uint32_t bits) { + return UnpackF16BitsToF32(state, bits, false); +} + +std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bits) { const auto u32 = TypeU32(state); const auto f32 = TypeF32(state); const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, u32, lhs, rhs); }; @@ -1211,21 +1226,45 @@ std::uint32_t EmitGetTessellationAttribute(SpirvValueEmitContext& ctx, const IrV }); } +namespace { + +std::uint32_t TessellationLevel(SpirvEmitterState& state, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state), pointer, state.pushConstantVariable, ConstantU32(state, 0u), ConstantU32(state, TessellationLevelPushOffsetBytes / 4u + index)); + const auto word = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), word, pointer); + const auto level = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeF32(state), level, word); + return level; +} + +std::uint32_t TessellationLevelBound(SpirvEmitterState& state, std::uint32_t level, std::uint32_t operation, float limit) { + const auto bounded = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), bounded, GlslStd450(state), operation, level, ConstantF32Value(state, limit)); + return bounded; +} + +} + void EmitSetTessellationAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitIfCondition(ctx.state, ctx.Arg(inst, 3), [&]() { auto value = ctx.Arg(inst, 2); if (inst.Argument(0)->ImmediateU32() == static_cast(TessellationAttribute::Factor)) { auto& state = ctx.state; - const bool outer = inst.Argument(1)->HasImmediate() && inst.Argument(1)->ImmediateU32() / 4u < 3u; + if (state.program.Resources().stage != IrShaderStage::TessellationControl || state.pushConstantVariable == 0u) ctx.Fail(inst, "writes tessellation factors outside a hull program"); + const bool outer = inst.Argument(1)->HasImmediate() && inst.Argument(1)->ImmediateU32() / 4u < TessellationOuterFactors(state); + const auto minimum = TessellationLevel(state, 0u); + const auto lowest = outer ? minimum : TessellationLevelBound(state, minimum, GLSLstd450FMax, 1.0f); const auto floating = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeF32(state), floating, value); - const auto lower = ConstantF32Value(state, outer ? 0.0f : 1.0f); const auto inside = state.module.AllocateId(); - state.module.AddFunction(spv::OpFOrdGreaterThan, TypeBool(state), inside, floating, lower); + state.module.AddFunction(spv::OpFOrdGreaterThan, TypeBool(state), inside, floating, ConstantF32Value(state, outer ? 0.0f : 1.0f)); + 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, floating, ConstantF32Value(state, 63.0f)); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMin, raised, TessellationLevelBound(state, TessellationLevel(state, 1u), GLSLstd450FMin, 63.0f)); value = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lower); + state.module.AddFunction(spv::OpSelect, TypeF32(state), value, inside, clamped, outer ? floating : lowest); } ctx.state.module.AddFunction(spv::OpStore, TessellationPointer(ctx, inst), value); }); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 2218a608ae..7f0ae8c42a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -184,6 +184,28 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct }); } +std::uint32_t byteBuffers(SpirvEmitterState& state) { + if (state.storageBufferU8Variable != 0u) return state.storageBufferU8Variable; + if (state.program.Resources().stage != IrShaderStage::Compute) return 0u; + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityStorageBuffer8BitAccess)) == state.supportedCapabilities.end()) return 0u; + const IrBindingLayout& layout = state.program.Metadata().bindings; + const auto binding = std::find_if(layout.descriptors.begin(), layout.descriptors.end(), [](const IrDescriptorBinding& descriptor) { return descriptor.kind == DescriptorBindingKind::Buffers; }); + if (binding == layout.descriptors.end() || state.storageBufferVariable == 0u) return 0u; + state.module.EmitCapability(spv::CapabilityStorageBuffer8BitAccess); + state.module.EmitExtension("SPV_KHR_8bit_storage"); + const auto count = ConstantU32(state, static_cast(binding->resources.size())); + const auto type = state.module.Type(spv::OpTypeArray, TypeStorageBufferU8Block(state), count); + const auto variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, type), spv::StorageClassStorageBuffer); + state.module.AddName(variable, "buffers_u8"); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(state.program.Resources().stage, DescriptorBindingKind::Buffers)); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + if (state.storageBufferU64Variable == 0u && state.storageBufferCoherentVariable == 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased); + if (state.requirements.coherentBuffers) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationCoherent); + state.storageBufferU8Variable = variable; + return variable; +} + void writeBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t value, std::uint32_t bits = 32u) { if (context.Memory(instruction).gpuDescriptor) { EmitBdaWrite(context, instruction, address, value, bits); @@ -197,6 +219,24 @@ void writeBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std const auto unaligned = nonzero(state, misalignment); const auto next = EmitAddU32(state, index, ConstantU32(state, 1u)); const auto last = Select(state, u32, unaligned, next, index); + const auto bytes = bits != 32u ? byteBuffers(state) : 0u; + if (bytes != 0u) { + const auto element = ResourceForDescriptor(state, DescriptorBindingKind::Buffers, context.Memory(instruction).resource); + const auto block = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferU8Pointer(state), block, bytes, ConstantU32(state, element)); + EmitIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { + const auto first = EmitAddU32(state, EmitAddU32(state, Binary(state, spv::OpShiftLeftLogical, u32, index, ConstantU32(state, 2u)), misalignment), EmitAndConstant(state, address, 3u)); + for (std::uint32_t byte = 0; byte < bits / 8u; ++byte) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferU8ElementPointer(state), pointer, block, ConstantU32(state, 0u), EmitAddU32(state, first, ConstantU32(state, byte))); + const auto shifted = byte == 0u ? value : Binary(state, spv::OpShiftRightLogical, u32, value, ConstantU32(state, byte * 8u)); + const auto stored = Unary(state, spv::OpUConvert, TypeU8(state), shifted); + if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpStore, pointer, stored, access.memoryAccess); + else state.module.AddFunction(spv::OpStore, pointer, stored); + } + }); + return; + } EmitIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { auto mask = ConstantU32(state, bits == 32u ? 0xffffffffu : (1u << bits) - 1u); auto masked = Binary(state, spv::OpBitwiseAnd, u32, value, mask); @@ -601,6 +641,11 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa auto& state = context.state; if (bits != 32u && bits != 64u) context.Fail(instruction, "unsupported runtime buffer atomic width"); if (context.Memory(instruction).gpuDescriptor && (state.bdaAtomicPointerFunction == 0u || state.bdaNoteWriteFunction == 0u)) context.Fail(instruction, "BDA atomic functions are missing"); + if (!context.Memory(instruction).gpuDescriptor) { + if (state.program.Resources().stage == IrShaderStage::TessellationControl) context.Fail(instruction, "BDA fault termination requires a barrier-safe tessellation-control execution protocol"); + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityInt64) == state.supportedCapabilities.end()) context.Fail(instruction, "buffer atomic fault addresses require shaderInt64"); + state.module.EmitCapability(spv::CapabilityInt64); + } const auto type = bits == 64u ? TypeU64(state) : TypeU32(state); const auto scalar = bits == 64u ? TypeScalarU64(state) : TypeU32(state); const auto zero = bits == 64u ? ConstantU64(state, 0u) : ConstantU32(state, 0u); @@ -613,7 +658,11 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa const auto byte = EmitAddU32(state, access.guest, storage.byteOffset); const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); - faultIf(context, instruction, unaligned, guest, bits / 8u, BdaAbi::FaultReason::Unaligned); + EmitIfCondition(state, unaligned, [&] { + const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); + RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); + }); + StopBdaInvocationIf(state, unaligned); const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, bits == 64u ? 3u : 2u)); return EmitValueOrDefaultIfCondition(state, both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), EmitMemoryElementInBounds(state, storage, index)), type, zero, [&] { const auto pointer = EmitStorageBufferElementPointer(state, storage, index, bits == 64u ? TypeStorageBufferU64ElementPointer(state) : TypeStorageBufferElementPointer(state)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 6d8d414774..d9cc99512a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -220,6 +220,209 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { return true; } +template +void VisitIdOperands(const Instruction& instruction, std::uint32_t switchLiteralWords, const TVisitor& visit) { + const auto size = instruction.size(); + const auto from = [&](std::size_t first) { + for (auto index = first; index < size; ++index) visit(index); + }; + const auto only = [&](std::initializer_list indices) { + for (const auto index : indices) { + if (index < size) visit(index); + } + }; + constexpr std::uint32_t knownMemoryAccess = static_cast(spv::MemoryAccessVolatileMask) | static_cast(spv::MemoryAccessAlignedMask) | + static_cast(spv::MemoryAccessNontemporalMask) | static_cast(spv::MemoryAccessMakePointerAvailableMask) | + static_cast(spv::MemoryAccessMakePointerVisibleMask) | static_cast(spv::MemoryAccessNonPrivatePointerMask); + const auto memoryOperands = [&](std::size_t index) { + while (index < size) { + const auto mask = instruction[index++]; + if ((mask & ~knownMemoryAccess) != 0u) { + from(index); + return; + } + if ((mask & static_cast(spv::MemoryAccessAlignedMask)) != 0u) ++index; + if ((mask & static_cast(spv::MemoryAccessMakePointerAvailableMask)) != 0u && index < size) visit(index++); + if ((mask & static_cast(spv::MemoryAccessMakePointerVisibleMask)) != 0u && index < size) visit(index++); + } + }; + switch (Opcode(instruction)) { + case spv::OpNop: + case spv::OpCapability: + case spv::OpExtension: + case spv::OpExtInstImport: + case spv::OpMemoryModel: + case spv::OpSource: + case spv::OpSourceContinued: + case spv::OpSourceExtension: + case spv::OpString: + case spv::OpName: + case spv::OpMemberName: + case spv::OpModuleProcessed: + case spv::OpDecorate: + case spv::OpMemberDecorate: + case spv::OpDecorateString: + case spv::OpMemberDecorateString: + case spv::OpTypeVoid: + case spv::OpTypeBool: + case spv::OpTypeInt: + case spv::OpTypeFloat: + case spv::OpTypeSampler: + case spv::OpConstantTrue: + case spv::OpConstantFalse: + case spv::OpConstant: + case spv::OpConstantSampler: + case spv::OpConstantNull: + case spv::OpSpecConstantTrue: + case spv::OpSpecConstantFalse: + case spv::OpSpecConstant: + case spv::OpLabel: + case spv::OpNoLine: + case spv::OpReturn: + case spv::OpUnreachable: + case spv::OpKill: + case spv::OpFunctionEnd: + return; + case spv::OpTypeVector: + case spv::OpTypeMatrix: + case spv::OpTypeImage: + only({2}); + return; + case spv::OpTypePointer: + only({3}); + return; + case spv::OpTypeForwardPointer: + case spv::OpExecutionMode: + case spv::OpLine: + case spv::OpSelectionMerge: + only({1}); + return; + case spv::OpSpecConstantOp: + only({1}); + from(4); + return; + case spv::OpVariable: + case spv::OpFunction: + only({1, 4}); + return; + case spv::OpLoad: + only({1, 3}); + memoryOperands(4); + return; + case spv::OpStore: + case spv::OpCopyMemory: + only({1, 2}); + memoryOperands(3); + return; + case spv::OpCopyMemorySized: + only({1, 2, 3}); + memoryOperands(4); + return; + case spv::OpArrayLength: + case spv::OpCompositeExtract: + only({1, 3}); + return; + case spv::OpCompositeInsert: + case spv::OpVectorShuffle: + only({1, 3, 4}); + return; + case spv::OpExtInst: + only({1, 3}); + from(5); + return; + case spv::OpEntryPoint: { + only({2}); + std::size_t index = 3; + while (index < size) { + const auto word = instruction[index++]; + if ((word & 0xffu) == 0u || (word & 0xff00u) == 0u || (word & 0xff0000u) == 0u || (word & 0xff000000u) == 0u) break; + } + from(index); + return; + } + case spv::OpExecutionModeId: + case spv::OpDecorateId: + only({1}); + from(3); + return; + case spv::OpLoopMerge: + only({1, 2}); + return; + case spv::OpBranchConditional: + only({1, 2, 3}); + return; + case spv::OpSwitch: + only({1, 2}); + for (std::size_t index = 3; index + switchLiteralWords < size; index += switchLiteralWords + 1u) visit(index + switchLiteralWords); + return; + case spv::OpImageSampleImplicitLod: + case spv::OpImageSampleExplicitLod: + case spv::OpImageSampleProjImplicitLod: + case spv::OpImageSampleProjExplicitLod: + case spv::OpImageFetch: + case spv::OpImageRead: + case spv::OpImageSparseSampleImplicitLod: + case spv::OpImageSparseSampleExplicitLod: + case spv::OpImageSparseSampleProjImplicitLod: + case spv::OpImageSparseSampleProjExplicitLod: + case spv::OpImageSparseFetch: + case spv::OpImageSparseRead: + only({1, 3, 4}); + from(6); + return; + case spv::OpImageSampleDrefImplicitLod: + case spv::OpImageSampleDrefExplicitLod: + case spv::OpImageSampleProjDrefImplicitLod: + case spv::OpImageSampleProjDrefExplicitLod: + case spv::OpImageGather: + case spv::OpImageDrefGather: + case spv::OpImageSparseSampleDrefImplicitLod: + case spv::OpImageSparseSampleDrefExplicitLod: + case spv::OpImageSparseSampleProjDrefImplicitLod: + case spv::OpImageSparseSampleProjDrefExplicitLod: + case spv::OpImageSparseGather: + case spv::OpImageSparseDrefGather: + only({1, 3, 4, 5}); + from(7); + return; + case spv::OpImageWrite: + only({1, 2, 3}); + from(5); + return; + case spv::OpGroupIAdd: + case spv::OpGroupFAdd: + case spv::OpGroupFMin: + case spv::OpGroupUMin: + case spv::OpGroupSMin: + case spv::OpGroupFMax: + case spv::OpGroupUMax: + case spv::OpGroupSMax: + case spv::OpGroupNonUniformBallotBitCount: + case spv::OpGroupNonUniformIAdd: + case spv::OpGroupNonUniformFAdd: + case spv::OpGroupNonUniformIMul: + case spv::OpGroupNonUniformFMul: + case spv::OpGroupNonUniformSMin: + case spv::OpGroupNonUniformUMin: + case spv::OpGroupNonUniformFMin: + case spv::OpGroupNonUniformSMax: + case spv::OpGroupNonUniformUMax: + case spv::OpGroupNonUniformFMax: + case spv::OpGroupNonUniformBitwiseAnd: + case spv::OpGroupNonUniformBitwiseOr: + case spv::OpGroupNonUniformBitwiseXor: + case spv::OpGroupNonUniformLogicalAnd: + case spv::OpGroupNonUniformLogicalOr: + case spv::OpGroupNonUniformLogicalXor: + only({1, 3}); + from(5); + return; + default: + from(1); + return; + } +} + struct ScalarType { std::uint32_t width; bool boolean; @@ -257,6 +460,8 @@ public: changed |= propagateCopies(); } removeDeadComputations(); + removeUnreachableBlocks(); + removeUnusedConstants(); orderPhis(); std::vector words = header; bool inserted = false; @@ -418,9 +623,12 @@ private: return true; }; std::set loopHeaders; + std::map continueHeaders; for (const auto& [label, indices] : blocks) { for (const auto index : indices) { - if (Opcode(instructions[index]) == spv::OpLoopMerge) loopHeaders.insert(label); + if (Opcode(instructions[index]) != spv::OpLoopMerge) continue; + loopHeaders.insert(label); + continueHeaders.emplace(instructions[index].at(2), label); } } std::map> edges; @@ -447,8 +655,8 @@ private: retainMerge = !linearSelection(target, instruction.at(1)); if (!retainMerge) instruction.clear(); } - const auto replacement = !retainMerge ? Make(spv::OpBranch, {target}) : op == spv::OpSwitch ? - Make(spv::OpSwitch, {terminal.at(1), target}) : Make(spv::OpBranchConditional, {terminal.at(1), target, target}); + const auto replacement = !retainMerge ? Make(spv::OpBranch, {target}) : op == spv::OpSwitch ? Make(spv::OpSwitch, {terminal.at(1), target}) : + header.at(1) >= 0x00010600u ? terminal : Make(spv::OpBranchConditional, {terminal.at(1), target, target}); if (terminal != replacement) { terminal = replacement; changed = true; @@ -475,7 +683,23 @@ private: const auto& successors = edges.at(label); pending.insert(pending.end(), successors.begin(), successors.end()); } + const auto backEdge = [&](std::uint32_t label) { + const auto header = continueHeaders.find(label); + return !live.contains(label) && header != continueHeaders.end() && live.contains(header->second) ? header->second : 0u; + }; for (const auto& [label, indices] : blocks) { + if (const auto header = backEdge(label)) { + const auto branch = Make(spv::OpBranch, {header}); + if (indices.size() == 2u && instructions[indices.back()] == branch) continue; + for (std::size_t index = 1; index < indices.size(); ++index) { + auto& instruction = instructions[indices[index]]; + if (const auto result = Result(instruction)) removed.insert(result); + instruction.clear(); + } + instructions[indices.back()] = branch; + changed = true; + continue; + } if (!live.contains(label)) { if (indices.size() == 2u && Opcode(instructions[indices.back()]) == spv::OpUnreachable) continue; for (std::size_t index = 1; index < indices.size(); ++index) { @@ -494,9 +718,10 @@ private: for (std::size_t operand = 3; operand + 1u < instruction.size(); operand += 2u) { const auto parent = instruction[operand + 1u]; if (live.contains(parent) && std::ranges::find(edges.at(parent), label) != edges.at(parent).end()) phi.insert(phi.end(), {instruction[operand], parent}); + else if (backEdge(parent) == label) phi.insert(phi.end(), {instruction[2], parent}); } if (phi.size() == 3u) throw std::runtime_error("reachable prepared phi has no predecessor"); - if (phi.size() == instruction.size() && phi.size() != 5u) continue; + if (phi == instruction && phi.size() != 5u) continue; phi[0] = (static_cast(phi.size()) << 16u) | spv::OpPhi; instruction = phi.size() == 5u ? Make(spv::OpCopyObject, {phi[1], phi[2], phi[3]}) : std::move(phi); changed = true; @@ -505,6 +730,53 @@ private: return changed; } + std::set referencedIds() const { + std::set referenced; + 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; + } + VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { referenced.insert(instruction[index]); }); + } + return referenced; + } + + void removeUnreachableBlocks() { + const auto referenced = referencedIds(); + const auto next = [&](std::size_t index) { + while (++index < instructions.size() && instructions[index].empty()) {} + return index; + }; + std::size_t previous = instructions.size(); + for (std::size_t index = 0; index < instructions.size(); ++index) { + if (instructions[index].empty()) continue; + const auto op = Opcode(instructions[index]); + const auto following = next(index); + const bool entry = previous < instructions.size() && (Opcode(instructions[previous]) == spv::OpFunction || Opcode(instructions[previous]) == spv::OpFunctionParameter); + if (op == spv::OpLabel && !entry && following < instructions.size() && Opcode(instructions[following]) == spv::OpUnreachable && !referenced.contains(instructions[index].at(1))) { + removed.insert(instructions[index].at(1)); + instructions[index].clear(); + instructions[following].clear(); + index = following; + continue; + } + previous = index; + } + } + + void removeUnusedConstants() { + const auto referenced = referencedIds(); + std::erase_if(constants, [&](const Instruction& constant) { + const auto id = Result(constant); + if (referenced.contains(id)) return false; + removed.insert(id); + return true; + }); + } + void orderPhis() { std::vector ordered; std::vector copies; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7d9fbb0c31..bc79d3fd18 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -92,6 +92,7 @@ private: IrU32 conditionBit(const RdnaOperand& operand); IrU1 readMask(const RdnaOperand& operand); IrU1 readMaskValid(const RdnaOperand& operand); + static bool maskLanesExact(const RdnaOperand& operand); std::array writeMask(const RdnaOperand& operand, IrU1 value, bool write64 = false); void writeCompareResult(const RdnaOperand& operand, IrU1 value); MemoryFlags addMemoryInfo(const MemoryInfo& memory, std::uint32_t pc); @@ -250,6 +251,8 @@ private: bool packedInteger16MinMax(const RdnaInstruction& inst, IrOpcode opcode, bool sign); bool sU64Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, IrOpcode bitOpcode, bool negateRhs, bool negateResult, bool unary); IrU1 u64MaskBinary(const RdnaInstruction& inst, IrOpcode opcode, bool negateRhs, bool negateResult); + template + bool sU32Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, bool binary, bool negateRhs, bool negateResult, TWord&& word); bool simpleInteger(const RdnaInstruction& inst, IrOpcode opcode, IrType type, bool reverse, bool maskShiftCount, bool updateScc); bool composedIntegerBinary(const RdnaInstruction& inst, IrOpcode opcode, bool negateRhs, bool negateResult, bool updateScc); bool vAndOrB32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index b2168d1167..18bb250332 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -60,6 +60,7 @@ bool SaveexecWritesDestinationFirst() { void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operation, bool negateExec, bool negateSource, bool write64, bool negateResult, bool writeResult) { // Callers name the lane-wise operation; the exec mask words themselves combine bitwise. + const IrOpcode laneOperation = operation; if (operation == IrOpcode::LogicalAnd) operation = IrOpcode::BitwiseAnd32; else if (operation == IrOpcode::LogicalOr) operation = IrOpcode::BitwiseOr32; else if (operation == IrOpcode::LogicalXor) operation = IrOpcode::BitwiseXor32; @@ -88,19 +89,40 @@ void TranslationContext::sSaveexec(const RdnaInstruction& inst, IrOpcode operati return; } const IrU32 oldExec(ir.GetExecLo()); + const IrU1 oldLane(ir.GetExec()); const IrU32 savedExec = hostExecWord(0u); - if (writeFirst) writeRawU32(inst.destination, savedExec); + const bool lanes = program.WaveSize() == 32u && (execSource || maskLanesExact(sourceAt(inst, 0u))); + const auto writeSaved = [&](IrU32 word, IrU1 lane) { + writeRawU32(inst.destination, word); + if (lanes && inst.destination.kind == RdnaOperandKind::ScalarRegister) { + const auto reg = static_cast(inst.destination.reg); + ir.SetThreadBitScalarReg(reg, lane.Value()); + ir.SetScalarMaskTag(reg, ir.ConstantBool(true)); + } + }; + if (writeFirst) writeSaved(savedExec, oldLane); const IrU32 source = execSource ? oldExec : readU32(sourceAt(inst, 0u)); - if (!writeFirst && !writeResult) writeRawU32(inst.destination, savedExec); + IrU1 sourceLane; + if (lanes) sourceLane = execSource ? oldLane : readMask(sourceAt(inst, 0u)); + if (!writeFirst && !writeResult) writeSaved(savedExec, oldLane); IrValue& execOperand = negateExec ? ir.BitwiseNot(oldExec.Value()) : oldExec.Value(); IrValue& sourceOperand = negateSource ? ir.BitwiseNot(source.Value()) : source.Value(); IrValue& combined = ir.Emit(operation, IrType::U32, {&execOperand, &sourceOperand}); const IrU32 newExec(negateResult ? ir.BitwiseNot(combined) : combined); ir.SetExecLo(newExec.Value()); - if (writeResult) writeRawU32(inst.destination, hostExecWord(0u)); + IrU1 newLane; + if (lanes) { + IrValue& execLane = negateExec ? ir.LogicalNot(oldLane.Value()) : oldLane.Value(); + IrValue& sourceLaneOperand = negateSource ? ir.LogicalNot(sourceLane.Value()) : sourceLane.Value(); + IrValue& combinedLane = ir.Emit(laneOperation, IrType::U1, {&execLane, &sourceLaneOperand}); + newLane = IrU1(negateResult ? ir.LogicalNot(combinedLane) : combinedLane); + } else { + newLane = threadBit({newExec, IrU32(ir.GetExecHi())}); + } + if (writeResult) writeSaved(hostExecWord(0u), newLane); const IrU1 nonZero(ir.INotEqual(newExec.Value(), ir.Constant(0u))); ir.SetScc(nonZero.Value()); - ir.SetExec(threadBit({newExec, IrU32(ir.GetExecHi())}).Value()); + ir.SetExec(newLane.Value()); } void TranslationContext::addU32(const RdnaInstruction& inst, bool vector, bool useCarryIn) { diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index e3455f44ea..0de004a63f 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -356,8 +356,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO setVector(static_cast(vgpr(PixelInput::PositionZ)), builtin(StageInputKind::FragCoord, 2u)); } if (loaded(PixelInput::PositionW)) { - IrValue& reciprocalW = entryIr.BitCastF32(builtin(StageInputKind::FragCoord, 3u)); - IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&reciprocalW}); + IrValue& reciprocalBits = builtin(StageInputKind::FragCoord, 3u); + IrValue& flushed = entryIr.Select(entryIr.IEqual(entryIr.BitwiseAnd(reciprocalBits, entryIr.Constant(0x7f800000u)), entryIr.Constant(0u)), entryIr.BitwiseAnd(reciprocalBits, entryIr.Constant(0x80000000u)), reciprocalBits); + IrValue& w = entryIr.Emit(IrOpcode::FPRecip32, IrOpcodeType(IrOpcode::FPRecip32), {&entryIr.BitCastF32(flushed)}); setVector(static_cast(vgpr(PixelInput::PositionW)), entryIr.BitCastU32(w)); } if (loaded(PixelInput::FrontFace)) { @@ -415,6 +416,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con IrProgram program; program.SetWaveSize(options.waveSize); std::uint32_t windowBase = NumVectorRegs; + bool indexedWrites = false; for (const auto& cfgBlock : cfg.blocks) { for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; @@ -425,11 +427,13 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con break; case RdnaOpcode::VMovreldB32: windowBase = std::min(windowBase, instruction.destination.reg); + indexedWrites = true; break; case RdnaOpcode::VMovrelsdB32: case RdnaOpcode::VMovrelsd2B32: case RdnaOpcode::VSwaprelB32: windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); + indexedWrites = true; break; default: break; @@ -440,6 +444,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con if (windowBase < vectorLimit && !ancillaryWindow) { program.Metadata().vectorWindowBase = windowBase; program.Metadata().vectorWindowSize = vectorLimit - windowBase; + program.Metadata().vectorWindowIndexedWrites = indexedWrites; } program.Resources().stage = toIrShaderStage(options.stage); program.Resources().shaderHash = options.shaderHash; diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 8484da2021..5762f88ccf 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -9,9 +9,39 @@ void TranslateScalarInstruction(IrBuilder& builder, const RdnaInstruction& instr throw std::runtime_error("TranslateScalarInstruction not implemented"); } +template +bool TranslationContext::sU32Mask(const RdnaInstruction& inst, IrOpcode logicalOpcode, bool binary, bool negateRhs, bool negateResult, TWord&& word) { + const auto& destination = inst.destination; + const auto execOrVcc = [](const RdnaOperand& operand) { return operand.kind == RdnaOperandKind::ExecLo || operand.kind == RdnaOperandKind::VccLo; }; + const bool involved = execOrVcc(destination) || execOrVcc(sourceAt(inst, 0u)) || (binary && execOrVcc(sourceAt(inst, 1u))); + const bool exact = maskLanesExact(sourceAt(inst, 0u)) && (!binary || maskLanesExact(sourceAt(inst, 1u))); + if (program.WaveSize() != 32u || !involved || !exact || (destination.kind != RdnaOperandKind::ScalarRegister && !execOrVcc(destination))) return word(); + IrU1 lane = binary ? u64MaskBinary(inst, logicalOpcode, negateRhs, negateResult) : readMask(sourceAt(inst, 0u)); + if (!binary && negateResult) lane = IrU1(ir.LogicalNot(lane.Value())); + IrU1 valid = readMaskValid(sourceAt(inst, 0u)); + if (binary) valid = IrU1(ir.LogicalAnd(valid.Value(), readMaskValid(sourceAt(inst, 1u)).Value())); + const bool result = word(); + switch (destination.kind) { + case RdnaOperandKind::ExecLo: + ir.SetExec(lane.Value()); + break; + case RdnaOperandKind::VccLo: + ir.SetVcc(lane.Value()); + break; + default: { + const auto reg = static_cast(destination.reg); + ir.SetThreadBitScalarReg(reg, lane.Value()); + ir.SetScalarMaskTag(reg, valid.Value()); + break; + } + } + return result; +} + bool TranslationContext::emitScalar(const RdnaInstruction& inst) { switch (inst.op) { case RdnaOpcode::SMovB32: + return sU32Mask(inst, IrOpcode::LogicalAnd, false, false, false, [&] { movB32(inst, false); return true; }); case RdnaOpcode::SMovkI32: movB32(inst, false); return true; @@ -281,13 +311,13 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SMulHiI32: return simpleInteger(inst, IrOpcode::SMulHi, IrType::U32, false, false, false); case RdnaOpcode::SAndB32: - return simpleInteger(inst, IrOpcode::BitwiseAnd32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseAnd32, IrType::U32, false, false, true); }); case RdnaOpcode::SOrB32: - return simpleInteger(inst, IrOpcode::BitwiseOr32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseOr32, IrType::U32, false, false, true); }); case RdnaOpcode::SXorB32: - return simpleInteger(inst, IrOpcode::BitwiseXor32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalXor, true, false, false, [&] { return simpleInteger(inst, IrOpcode::BitwiseXor32, IrType::U32, false, false, true); }); case RdnaOpcode::SNotB32: - return simpleInteger(inst, IrOpcode::BitwiseNot32, IrType::U32, false, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, false, false, true, [&] { return simpleInteger(inst, IrOpcode::BitwiseNot32, IrType::U32, false, false, true); }); case RdnaOpcode::SBrevB32: return simpleInteger(inst, IrOpcode::BitReverse32, IrType::U32, false, false, false); case RdnaOpcode::SBrevB64: { @@ -345,15 +375,15 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SLshrB64: return simpleInteger(inst, IrOpcode::ShiftRightLogical64, IrType::U64, false, false, true); case RdnaOpcode::SAndn2B32: - return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, true, false, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, true, false, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, true, false, true); }); case RdnaOpcode::SOrn2B32: - return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, true, false, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, true, false, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, true, false, true); }); case RdnaOpcode::SNandB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalAnd, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseAnd32, false, true, true); }); case RdnaOpcode::SNorB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalOr, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseOr32, false, true, true); }); case RdnaOpcode::SXnorB32: - return composedIntegerBinary(inst, IrOpcode::BitwiseXor32, false, true, true); + return sU32Mask(inst, IrOpcode::LogicalXor, true, false, true, [&] { return composedIntegerBinary(inst, IrOpcode::BitwiseXor32, false, true, true); }); case RdnaOpcode::SFf0I32B64: return sFfI32B64(inst, true); case RdnaOpcode::SFf1I32B64: diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ce067c4913..5eab8b6736 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -163,6 +163,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues vertexStorage.logicalStage = _toIrShaderStage(stage); vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; + vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); vertexStorage.fetchBufferReg = static_cast(vertex.fetchBufferReg); @@ -179,13 +180,15 @@ 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->partitioning != 2u || tessellation->outputTopology != 2u) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + 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->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; target.domain = tessellation->domain; target.partitioning = tessellation->partitioning; target.outputTopology = tessellation->outputTopology; + target.patchesPerGroup = tessellation->patchesPerGroup; } 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 6e94905dc9..29e92ea915 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp @@ -48,7 +48,7 @@ IrU1 TranslationContext::readMask(const RdnaOperand& operand) { } case RdnaOperandKind::ExecLo: case RdnaOperandKind::ExecHi: return IrU1(ir.GetExec()); - case RdnaOperandKind::VccLo: + case RdnaOperandKind::VccLo: return IrU1(ir.GetVcc()); case RdnaOperandKind::VccHi: return program.WaveSize() == 32u ? threadBit({readRawU32(operand), IrU32(ir.Constant(0u))}) : IrU1(ir.GetVcc()); case RdnaOperandKind::Scc: return IrU1(ir.GetScc()); case RdnaOperandKind::VccZ: return IrU1(ir.LogicalNot(ir.GetVcc())); @@ -57,6 +57,19 @@ IrU1 TranslationContext::readMask(const RdnaOperand& operand) { } } +bool TranslationContext::maskLanesExact(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::ScalarRegister: + case RdnaOperandKind::ExecLo: + case RdnaOperandKind::VccLo: + case RdnaOperandKind::VccHi: + case RdnaOperandKind::LiteralConstant: + case RdnaOperandKind::IntegerInlineConstant: + case RdnaOperandKind::FloatInlineConstant: return true; + default: return false; + } +} + IrU1 TranslationContext::readMaskValid(const RdnaOperand& operand) { if (operand.kind == RdnaOperandKind::LiteralConstant || operand.kind == RdnaOperandKind::IntegerInlineConstant || operand.kind == RdnaOperandKind::FloatInlineConstant) { const bool zero = operand.value == 0u; @@ -106,7 +119,12 @@ std::array TranslationContext::writeMask(const RdnaOperand& operand, I case RdnaOperandKind::VccLo: case RdnaOperandKind::VccHi: if (!write64 && program.WaveSize() == 32u) { - writeRawU32(operand, mask[0]); + if (operand.kind != RdnaOperandKind::VccLo) { + writeRawU32(operand, mask[0]); + return mask; + } + ir.SetVccLo(mask[0].Value()); + ir.SetVcc(value.Value()); return mask; } ir.SetVcc(value.Value()); diff --git a/demo/web/app.css b/demo/web/app.css new file mode 100644 index 0000000000..c5258a1102 --- /dev/null +++ b/demo/web/app.css @@ -0,0 +1,6 @@ +:root{color-scheme:light;--bg:#f9fafb;--surface:#fff;--sunken:#f2f3f5;--line:#e1e3e6;--text:#151619;--muted:#67696c;--accent:#245fd4;--on-accent:#fff;--warning:#875410;--warning-bg:#fff8e8;--error:#a52222;--error-bg:#fff0f0;--good:#218740} +@media(prefers-color-scheme:dark){:root:not([data-theme=light]){color-scheme:dark;--bg:#0d0e10;--surface:#161719;--sunken:#131416;--line:#2a2b2e;--text:#eff0f2;--muted:#a0a2a6;--accent:#7baaff;--on-accent:#0b0f18;--warning:#e4bb74;--warning-bg:#262117;--error:#ffa6a6;--error-bg:#2d191b;--good:#4fb768}} +:root[data-theme=dark]{color-scheme:dark;--bg:#0d0e10;--surface:#161719;--sunken:#131416;--line:#2a2b2e;--text:#eff0f2;--muted:#a0a2a6;--accent:#7baaff;--on-accent:#0b0f18;--warning:#e4bb74;--warning-bg:#262117;--error:#ffa6a6;--error-bg:#2d191b;--good:#4fb768} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,input,select{font:inherit}button,a,select,input{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.45}a{color:var(--accent);text-underline-offset:3px}button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:4px}code,pre,.eyebrow,.fine{font-family:ui-monospace,"Cascadia Mono",Consolas,monospace}code{font-size:.9em}h1,h2,h3,p{margin-top:0}[hidden]{display:none!important}header{border-bottom:1px solid var(--line);background:var(--surface)}.bar{max-width:1120px;margin:auto;min-height:68px;padding:12px 24px;display:flex;align-items:center;justify-content:space-between;gap:24px}.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--text);font-weight:700;font-size:17px;white-space:nowrap}.brand>span:last-child{font-weight:400;color:var(--muted);font-size:14px}.mark{display:flex;gap:3px;align-items:center;margin-right:3px}.mark i{display:block;width:4px;height:20px;background:var(--text)}.mark i:last-child{opacity:.3}nav{display:flex;align-items:center;gap:24px;font-size:13px}nav a{color:var(--muted);text-decoration:none}nav a:hover{color:var(--text)}main{max-width:1024px;margin:auto;padding:58px 24px 36px}.intro{max-width:660px;margin-bottom:30px}.eyebrow{color:var(--muted);letter-spacing:.12em;font-size:11px;margin-bottom:12px}h1{font-size:clamp(29px,4vw,43px);line-height:1.15;letter-spacing:-.035em;font-weight:650;margin-bottom:18px}.converter{border:1px solid var(--line);border-radius:10px;background:var(--surface);overflow:hidden}.panel-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;border-bottom:1px solid var(--line);padding:19px 26px}.panel-heading h2{font-size:15px;margin:0;font-weight:600}.private{font-size:12px;color:var(--muted)}.private b{color:var(--good);font-size:9px;margin-right:5px}.drop{margin:24px;border:1px dashed var(--line);border-radius:6px;text-align:center;padding:28px 16px 17px;background:var(--bg)}.drop.dragging{border-color:var(--accent);background:var(--sunken)}.archive-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:45px;border:1px solid var(--line);border-radius:4px;font-size:25px;color:var(--muted);margin-bottom:15px}.drop h3{font-size:19px;margin-bottom:8px;font-weight:600}.drop>p{color:var(--muted);font-size:13px;margin-bottom:20px}.button{display:inline-flex;align-items:center;justify-content:center;gap:18px;border:1px solid transparent;border-radius:6px;background:var(--accent);color:var(--on-accent);text-decoration:none;font-weight:600;font-size:13px;padding:11px 20px;white-space:nowrap}.button:hover:not(:disabled){filter:brightness(.94)}.drop .fine{margin:15px 0 0;font-size:10px}.fine{font-size:11px;line-height:1.6;color:var(--muted);margin-bottom:0}.plain{border:0;background:none;color:var(--muted);padding:8px;font-size:13px}.plain:hover{color:var(--text)}#theme{font-size:19px;padding:3px}.settings{display:grid;grid-template-columns:1fr 1fr;gap:36px;padding:0 26px 20px}fieldset{border:0;margin:0;padding:0;min-width:0}legend,.save-settings>label{display:block;font-size:12px;font-weight:600;margin-bottom:10px}.choices{display:flex;gap:10px}.choices label{flex:1;display:flex;gap:9px;align-items:center;border:1px solid var(--line);border-radius:6px;padding:10px;font-size:13px;cursor:pointer}.choices label:has(input:checked){border-color:var(--accent)}.choices span{font-size:11px;color:var(--muted);margin-left:auto}input{accent-color:var(--accent)}input[type=radio],input[type=checkbox]{margin:0;width:15px;height:15px;flex-shrink:0}select{width:100%;padding:10px;border:1px solid var(--line);border-radius:6px;background:var(--surface);color:var(--text);font-size:13px}.save-settings .fine{margin-top:8px}.options{padding:0 26px 23px;font-size:12px;color:var(--muted)}.options label{display:flex;gap:9px;align-items:center}.action-row{border-top:1px solid var(--line);padding:17px 26px;display:flex;gap:12px;align-items:center}.action-row>p{margin:0 auto 0 0;font-size:12px}.muted{color:var(--muted)}.selection{display:flex;justify-content:space-between;align-items:center;padding:25px 26px;gap:16px}.selection h3{font-size:18px;margin-bottom:6px;overflow-wrap:anywhere}.selection p{margin-bottom:0;font-size:12px}.selection .eyebrow{font-size:10px;margin-bottom:6px}.warnings,.notice{padding:16px 20px;background:var(--warning-bg);color:var(--warning);font-size:13px;border:1px solid var(--line);border-radius:6px}.warnings{margin:0 26px 24px}.warnings ul{padding-left:18px;margin:0 0 12px}.warnings label{display:flex;gap:8px;align-items:center}.notice{margin-bottom:20px}.status{border-top:1px solid var(--line);padding:22px 26px}.status-line{display:flex;justify-content:space-between;font-size:13px;gap:16px}progress{appearance:none;width:100%;height:5px;margin:14px 0 8px;border:0;border-radius:3px;overflow:hidden;background:var(--line)}progress::-webkit-progress-bar{background:var(--line)}progress::-webkit-progress-value{background:var(--accent)}progress::-moz-progress-bar{background:var(--accent)}#current-file{overflow-wrap:anywhere;min-height:18px}details{margin-top:18px;font-size:11px;color:var(--muted)}summary{cursor:pointer}pre{max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px}.error{margin:0 26px 24px;padding:15px;background:var(--error-bg);color:var(--error);border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere}.done{padding:24px 26px;border-top:1px solid var(--line)}.done h3{font-size:20px;color:var(--good);margin-bottom:8px}.done p{font-size:13px;color:var(--muted)}.done .fine{margin:15px 0 0}.compatibility{margin:12px 0 0;color:var(--muted);font-size:11px;line-height:1.8}.limits{border-top:1px solid var(--line);padding-top:22px;margin:30px 0 0;color:var(--muted);font-size:11px;line-height:1.8}footer{max-width:1024px;margin:auto;border-top:1px solid var(--line);padding:22px 24px;display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}footer div{display:flex;gap:20px}footer a{color:var(--muted);text-decoration:none}@media(max-width:680px){.bar{flex-wrap:wrap;gap:12px;padding:14px 20px}nav{gap:17px;font-size:12px}main{padding:34px 16px 26px}.panel-heading{padding:16px}.private{font-size:10px}.drop{margin:16px}.settings{grid-template-columns:1fr;padding:0 16px 18px;gap:22px}.options{padding:0 16px 20px}.action-row{padding:16px;flex-wrap:wrap}.action-row>p{width:100%}.selection{padding:20px 16px}.warnings{margin:0 16px 20px}.status,.done{padding:20px 16px}.error{margin:0 16px 20px}footer{flex-wrap:wrap;gap:12px}footer div{flex-wrap:wrap;gap:14px}} + +.input-actions{display:flex;justify-content:center;flex-wrap:wrap;gap:12px} diff --git a/demo/web/app.mjs b/demo/web/app.mjs new file mode 100644 index 0000000000..ca615cf880 --- /dev/null +++ b/demo/web/app.mjs @@ -0,0 +1,235 @@ +const $ = id => document.getElementById(id); +const stages = ["Converting executable", "Converting PRX modules", "Scanning syscalls", "Scanning instructions", "Converting instructions", "Analysing imports", "Building output image", "Writing output"]; +let worker; +let file; +let summary; +let working = false; +let sink; +let outputDirectory; +let downloadUrl; +let logs = []; +let release; +const canSaveZip = typeof window.showSaveFilePicker === "function" && window.isSecureContext; +const canStream = typeof window.showDirectoryPicker === "function" && window.isSecureContext; +const supported = window.isSecureContext && window.Worker && window.WebAssembly && window.ReadableStream && window.WritableStream && window.crypto?.subtle; +$("save").querySelector('[value="disk"]').disabled = !canStream; +$("save").querySelector('[value="zip"]').disabled = !canSaveZip; +$("save").value = canSaveZip ? "zip" : "memory"; + +function bytes(value) { + if (value < 1024) return `${value} B`; + const unit = value >= 1024 ** 3 ? 3 : value >= 1024 ** 2 ? 2 : 1; + return `${(value / 1024 ** unit).toFixed(1)} ${["B", "KiB", "MiB", "GiB"][unit]}`; +} + +function update() { + const target = document.querySelector('input[name="target"]:checked').value; + const folderBlocked = target === "windows" && $("save").value === "disk"; + const folderOption = $("save").querySelector('[value="disk"]'); + folderOption.disabled = !canStream || target === "windows"; + folderOption.textContent = target === "windows" ? "Save to folder · unavailable for Windows (DLL restrictions)" : "Save to folder · uncompressed"; + const libraryBytes = release?.platforms[target]?.expandedBytes; + const estimate = summary && libraryBytes !== undefined ? Math.ceil(summary.totalBytes * 1.02) + summary.selectedBytes * 4 + libraryBytes + summary.files * 1024 + 8 * 1024 * 1024 : null; + const tooLarge = estimate !== null && estimate > 256 * 1024 * 1024; + $("save").querySelector('[value="memory"]').disabled = Boolean(tooLarge); + const memoryBlocked = $("save").value === "memory" && tooLarge; + $("save-note").textContent = $("save").value === "disk" ? "Creates a subfolder with the executable, libs and app0. No ZIP compression. Browsers may block DLL and other restricted files; use ZIP for those apps." : $("save").value === "zip" ? "Writes a compressed ZIP directly to disk without holding the whole archive in memory." : "Up to 256 MiB including expanded files and output overhead."; + $("browser-note").hidden = supported && canStream && canSaveZip && !memoryBlocked && !folderBlocked; + $("browser-note").textContent = !supported ? "This browser or connection does not support the converter. Use a current browser over HTTPS, or use the native relinker." : folderBlocked ? "Browsers block direct DLL creation. Windows output requires ZIP. Choose Save ZIP to disk or Download in memory." : memoryBlocked ? "This archive exceeds the memory-download limit. Choose Save ZIP to disk. If that option is unavailable, open this page in desktop Chrome or Edge." : "Some direct disk saving options are unavailable in this browser or context. Small apps can still be downloaded in memory. For larger apps, use desktop Chrome or Edge over HTTPS."; + $("convert").disabled = !supported || working || !summary || !release || memoryBlocked || folderBlocked || (summary.warnings.length > 0 && !$("acknowledge").checked); + $("ready-note").textContent = summary ? (folderBlocked ? "Choose a ZIP save mode for Windows." : memoryBlocked ? "A direct disk save is required for this archive." : release ? `Libraries: ${release.tag} · ${$("save").value === "disk" ? "Uncompressed folder" : "ZIP compression level 9"}` : "Loading release information…") : "Choose a ZIP or folder to get started."; +} + +function lock(value) { + for (const id of ["choose", "choose-folder", "change", "target", "save", "intel"]) $(id).disabled = value || !supported || (id === "choose-folder" && !("webkitdirectory" in $("folder"))); +} + +async function fail(message) { + worker?.terminate(); + worker = null; + working = false; + summary = null; + if (sink) { + const active = sink; + sink = null; + try { await active.abort(); } catch (error) { message += `\nCould not discard the output file: ${error.message}`; } + } + if (outputDirectory) { message += `\nIncomplete files remain in ${outputDirectory.name}/.`; outputDirectory = null; } + $("error").textContent = message; + $("error").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("drop").hidden = false; + $("file").value = ""; + lock(false); + update(); +} + +async function onMessage({ data }) { + try { + if (data.type === "progress") { + if (data.message) { + logs.push(data.message.trim()); + logs = logs.slice(-150); + $("log").textContent = logs.join("\n"); + return; + } + $("phase").textContent = data.phase ?? stages[data.stage]; + $("current-file").textContent = data.input ?? ""; + if (Number.isFinite(data.total) && data.total > 0) { + const percent = Math.min(100, Math.floor(data.completed / data.total * 100)); + $("progress").value = percent; + $("percent").textContent = `${percent}%`; + } else { + $("progress").removeAttribute("value"); + $("percent").textContent = ""; + } + } else if (data.type === "inspected") { + summary = data.summary; + working = false; + $("selection").hidden = false; + $("drop").hidden = true; + $("archive-name").textContent = file.name; + $("archive-info").textContent = `${summary.files} files · ${bytes(summary.totalBytes)} expanded · ${summary.modules} modules · ${bytes(summary.selectedBytes)} conversion input` + (summary.root ? ` · Root: ${summary.root}` : ""); + $("warning-list").replaceChildren(...summary.warnings.map(message => { const item = document.createElement("li"); item.textContent = message; return item; })); + $("warnings").hidden = !summary.warnings.length; + $("status").hidden = true; + $("cancel").hidden = true; + lock(false); + update(); + } else if (data.type === "directory" || data.type === "open-file") { + if (!outputDirectory) throw new Error("No output folder is open"); + const parts = data.name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("Invalid output path"); + const filename = data.type === "open-file" ? parts.pop() : null; + let parent = outputDirectory; + for (const part of parts) parent = await parent.getDirectoryHandle(part, { create: true }); + if (filename) { + try { + const handle = await parent.getFileHandle(filename, { create: true }); + sink = await handle.createWritable(); + } catch (error) { + throw new Error(`Cannot write ${data.name}: ${error.message}. Choose ZIP output if the browser blocks this file type.`); + } + } + worker?.postMessage({ type: "written" }); + } else if (data.type === "close-file") { + if (!sink) throw new Error("No output file is open"); + await sink.close(); + sink = null; + worker?.postMessage({ type: "written" }); + } else if (data.type === "write") { + if (!sink) throw new Error("No output file is open"); + await sink.write(data.bytes); + worker?.postMessage({ type: "written" }); + } else if (data.type === "complete") { + if (sink) { await sink.close(); sink = null; } + working = false; + $("done").hidden = false; + $("status").hidden = true; + $("cancel").hidden = true; + $("result-info").textContent = `${bytes(data.size)} ${data.mode === "disk" ? "files" : "ZIP"} · ${data.release} system libraries · ${data.outputName}`; + if (data.blob) { + downloadUrl = URL.createObjectURL(data.blob); + $("download").href = downloadUrl; + $("download").download = `${data.outputName}.zip`; + $("download").hidden = false; + } else if (data.mode === "zip") { + $("result-info").textContent += " · ZIP saved to disk"; + } else { + $("result-info").textContent += ` · Saved in ${outputDirectory.name}/`; + if (document.querySelector('input[name="target"]:checked').value === "linux") $("result-info").textContent += ` · On Linux, run chmod +x "${data.outputName}" before launching.`; + } + $("result-note").hidden = data.mode === "disk"; + outputDirectory = null; + summary = null; + worker.terminate(); + worker = null; + lock(false); + update(); + } else if (data.type === "error") await fail(data.message); + } catch (error) { await fail(error.message); } +} + +async function selectFile(selected, files) { + if (working || !supported || !selected) return; + if (!files && !selected.name.toLowerCase().endsWith(".zip")) { await fail("Choose a ZIP archive."); return; } + worker?.terminate(); + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + summary = null; + file = selected; + working = true; + logs = []; + $("log").textContent = ""; + for (const id of ["error", "done", "download", "warnings", "selection"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("status").hidden = false; + $("cancel").hidden = false; + $("phase").textContent = "Inspecting files"; + $("current-file").textContent = file.name; + $("percent").textContent = ""; + $("progress").removeAttribute("value"); + lock(true); + update(); + worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" }); + worker.onmessage = onMessage; + worker.onerror = event => fail(event.message || "The converter worker stopped. Try a smaller app or use the native relinker."); + worker.postMessage(files ? { type: "inspect", files } : { type: "inspect", file }); +} + +$("choose").onclick = () => $("file").click(); +$("change").onclick = () => { $("drop").hidden = false; }; +$("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; +$("folder").onchange = () => { + const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); + if (files.length) selectFile({ name: files[0].path.split("/")[0] }, files); +}; +$("file").onchange = () => selectFile($("file").files[0]); +for (const type of ["dragenter", "dragover"]) $("drop").addEventListener(type, event => { event.preventDefault(); if (!working) $("drop").classList.add("dragging"); }); +$("drop").ondragleave = () => $("drop").classList.remove("dragging"); +$("drop").ondrop = event => { event.preventDefault(); $("drop").classList.remove("dragging"); if (working) return; if (event.dataTransfer.files.length !== 1) { fail("Drop one ZIP archive at a time."); return; } selectFile(event.dataTransfer.files[0]); }; +for (const id of ["save", "target", "acknowledge"]) $(id).addEventListener("change", update); +$("cancel").onclick = () => fail("Cancelled. Choose the ZIP or folder again to restart."); +$("convert").onclick = async () => { + if ($("convert").disabled) return; + working = true; + lock(true); + update(); + try { + if ($("save").value === "zip") { + const handle = await window.showSaveFilePicker({ suggestedName: file.name.replace(/\.zip$/iu, "") + "-" + document.querySelector('input[name="target"]:checked').value + ".zip", types: [{ description: "ZIP archive", accept: { "application/zip": [".zip"] } }] }); + sink = await handle.createWritable(); + } else if ($("save").value === "disk") { + const parent = await window.showDirectoryPicker({ mode: "readwrite" }); + const name = file.name.replace(/\.zip$/iu, "") + "-" + document.querySelector('input[name="target"]:checked').value; + let exists = false; + try { await parent.getDirectoryHandle(name); exists = true; } + catch (error) { if (error.name !== "NotFoundError") throw error; } + if (exists) throw new Error(`The folder ${name} already exists. Choose another destination or rename the existing folder.`); + outputDirectory = await parent.getDirectoryHandle(name, { create: true }); + } + $("status").hidden = false; + $("cancel").hidden = false; + $("error").hidden = true; + worker.postMessage({ type: "convert", target: document.querySelector('input[name="target"]:checked').value, toIntel: $("intel").checked, mode: $("save").value }); + } catch (error) { + if (error.name === "AbortError") { working = false; lock(false); update(); } + else await fail(error.message); + } +}; +$("theme").onclick = () => { + const dark = document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches; + document.documentElement.dataset.theme = dark ? "light" : "dark"; +}; +update(); +lock(false); +if (supported) fetch("./release.json", { cache: "no-cache" }).then(response => { if (!response.ok) throw new Error(`Cannot load release information (${response.status})`); return response.json(); }).then(value => { + if (!value.tag || !["windows", "linux"].every(target => Number.isSafeInteger(value.platforms?.[target]?.expandedBytes))) throw new Error("Invalid release information"); + release = value; + update(); +}).catch(error => fail(error.message)); + +if (!("webkitdirectory" in $("folder"))) { + $("choose-folder").disabled = true; + $("choose-folder").title = "Folder selection is unavailable in this browser. Choose a ZIP instead."; +} diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs new file mode 100644 index 0000000000..20c9584823 --- /dev/null +++ b/demo/web/archive.mjs @@ -0,0 +1,295 @@ +import { BlobReader, Uint8ArrayReader, ZipReader, ZipWriter, configure } from "https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.57/lib/zip-no-worker.js/+esm"; + +configure({ useWebWorkers: false, useCompressionStream: false, chunkSize: 65536 }); + +export const MEMORY_LIMIT = 256 * 1024 * 1024; +export const CONVERSION_LIMIT = 256 * 1024 * 1024; +export const ICON_LIMIT = 16 * 1024 * 1024; +const MAX_ENTRIES = 100000; + +class BoundedBlobReader extends BlobReader { + async readUint8Array(offset, length) { + if (length > 16 * 1024 * 1024) throw new Error("ZIP directory metadata exceeds 16 MiB"); + return super.readUint8Array(offset, length); + } +} + + +export function validPath(name) { + if (!name || name.includes("\\") || /[\x00-\x1f<>:"|?*]/u.test(name)) throw new Error(`Unsafe ZIP path: ${name}`); + const parts = name.replace(/\/$/u, "").split("/"); + if (parts.some(part => !part || part === "." || part === ".." || /[. ]$/u.test(part) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part))) throw new Error(`Unsafe ZIP path: ${name}`); + return name; +} + +function safeSize(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new Error("ZIP contains an unsupported file size"); + return value; +} + +async function signature(entry) { + if (entry.uncompressedSize < 4) return false; + const stop = new AbortController(); + const header = new Uint8Array(4); + let filled = 0; + try { + await entry.getData(new WritableStream({ + write(chunk) { + const length = Math.min(4 - filled, chunk.length); + header.set(chunk.subarray(0, length), filled); + filled += length; + if (filled === 4) stop.abort(); + } + }), { signal: stop.signal, checkSignature: false }); + } catch (error) { + if (!stop.signal.aborted || filled !== 4) throw error; + } + return header[0] === 0x7f && header[1] === 69 && header[2] === 76 && header[3] === 70; +} + +async function boundedBytes(entry, limit) { + if (entry.uncompressedSize > limit) throw new Error(`${entry.filename} exceeds the conversion memory limit`); + let size = 0; + const chunks = []; + await entry.getData(new WritableStream({ + write(chunk) { + size += chunk.byteLength; + if (size > limit || size > entry.uncompressedSize) throw new Error(`Unexpected expanded size: ${entry.filename}`); + chunks.push(chunk); + } + }), { checkSignature: true }); + if (size !== entry.uncompressedSize) throw new Error(`Incorrect expanded size: ${entry.filename}`); + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length; } + return bytes; +} + +class FolderReader { + constructor(files) { + this.files = files; + } + + async *getEntriesGenerator() { + for (const { path, file } of this.files) { + if (!(file instanceof Blob)) throw new Error(`Invalid folder file: ${path}`); + yield { + filename: path, uncompressedSize: file.size, directory: false, compressionMethod: 0, + externalFileAttributes: (0o100644 << 16) >>> 0, + getData(destination, options = {}) { + return file.stream().pipeTo(destination, { signal: options.signal }); + } + }; + } + } + + async close() {} +} + +export class Archive { + constructor(file) { + this.reader = Array.isArray(file) ? new FolderReader(file) : new ZipReader(new BoundedBlobReader(file)); + this.entries = []; + } + + async inspect(report) { + const paths = new Map(); + let total = 0; + for await (const entry of this.reader.getEntriesGenerator()) { + if (this.entries.length >= MAX_ENTRIES) throw new Error("Input contains more than 100,000 entries"); + validPath(entry.filename); + const key = entry.filename.replace(/\/$/u, "").toLowerCase(); + if (paths.has(key)) throw new Error(`Duplicate ZIP path: ${entry.filename}`); + if (entry.encrypted) throw new Error(`Encrypted ZIP entries are not supported: ${entry.filename}`); + if (!entry.directory && entry.compressionMethod !== 0 && entry.compressionMethod !== 8) throw new Error(`Unsupported ZIP compression: ${entry.filename}`); + const mode = (entry.externalFileAttributes >>> 16) & 0xf000; + if (mode && mode !== 0x8000 && mode !== 0x4000) throw new Error(`Links and special files are not supported: ${entry.filename}`); + paths.set(key, entry.directory); + total = safeSize(total + safeSize(entry.uncompressedSize)); + this.entries.push(entry); + } + if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); + for (const entry of this.entries) { + const parts = entry.filename.split("/"); + for (let index = 1; index < parts.length; index++) { + if (paths.get(parts.slice(0, index).join("/").toLowerCase()) === false) throw new Error(`A file is also used as a folder: ${entry.filename}`); + } + } + let prefix = ""; + const files = this.entries.filter(entry => !entry.directory); + while (files.every(entry => entry.filename.slice(prefix.length).includes("/"))) { + const first = files[0].filename.slice(prefix.length).split("/")[0]; + if (["sce_sys", "sce_module"].includes(first) || !this.entries.every(entry => entry.filename === prefix || entry.filename.startsWith(prefix + first + "/"))) break; + prefix += first + "/"; + } + this.prefix = prefix; + this.files = this.entries.filter(entry => entry.filename.startsWith(prefix) && entry.filename !== prefix).map(entry => ({ entry, name: entry.filename.slice(prefix.length) })); + const executables = []; + this.modules = []; + for (let index = 0; index < this.files.length; index++) { + const item = this.files[index]; + report({ phase: "Inspecting files", input: item.name, completed: index, total: this.files.length }); + if (item.entry.directory || item.name.startsWith("sce_sys/")) continue; + if (!item.name.startsWith("sce_module/") && item.name.includes("/")) continue; + if (item.name.startsWith("sce_module/") && item.name.endsWith(".guest.prx")) continue; + const elf = await signature(item.entry); + if (item.name.startsWith("sce_module/")) { + if (elf) this.modules.push(item); + } else if (elf) executables.push(item); + } + if (executables.length !== 1) throw new Error(`Expected exactly one executable ELF in the app root; found ${executables.length}`); + this.executable = executables[0]; + if (this.executable.name.includes("/")) throw new Error("Place the executable next to sce_sys and sce_module, not inside another folder"); + this.icon = this.files.find(item => item.name === "sce_sys/icon0.png" && !item.entry.directory); + if (this.icon && this.icon.entry.uncompressedSize > ICON_LIMIT) throw new Error("sce_sys/icon0.png exceeds 16 MiB"); + this.selectedBytes = [this.executable, ...this.modules, ...(this.icon ? [this.icon] : [])].reduce((sum, item) => sum + item.entry.uncompressedSize, 0); + if (this.selectedBytes > CONVERSION_LIMIT) throw new Error("Executable, modules and icon exceed the 256 MiB conversion limit. Use the native relinker for this app."); + const warnings = []; + if (!this.files.some(item => item.name.startsWith("sce_sys/"))) warnings.push("sce_sys is missing. The Windows executable will have no app icon."); + else if (!this.icon) warnings.push("sce_sys/icon0.png is missing. The Windows executable will have no app icon."); + if (!this.files.some(item => item.name.startsWith("sce_module/"))) warnings.push("sce_module is missing. If the original app included this folder, leaving it out can prevent the app from starting."); + this.totalBytes = total; + return { name: this.executable.name, files: files.length, modules: this.modules.length, totalBytes: total, selectedBytes: this.selectedBytes, root: prefix, warnings }; + } + + async conversionInput(target, toIntel, report) { + let remaining = CONVERSION_LIMIT; + const read = async item => { + report({ phase: "Reading conversion input", input: item.name }); + const bytes = await boundedBytes(item.entry, remaining); + remaining -= bytes.length; + return bytes; + }; + const name = this.executable.name; + const stem = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name; + const outputName = (stem || "app") + (target === "windows" ? ".exe" : ".elf"); + const input = { name, outputName, target, toIntel, executable: await read(this.executable), modules: [], icon: null }; + for (const item of this.modules) input.modules.push({ name: item.name, bytes: await read(item) }); + if (this.icon && target === "windows") input.icon = await read(this.icon); + return input; + } + + async close() { await this.reader.close(); } +} + +export function memoryEstimate(archiveBytes, selectedBytes, libraryBytes, entries) { + return safeSize(Math.ceil(archiveBytes * 1.02) + selectedBytes * 4 + libraryBytes + entries * 1024 + 8 * 1024 * 1024); +} + +async function appendEntry(writer, name, entry, report) { + const stream = new TransformStream(); + let produced = 0; + const limit = new TransformStream({ transform(chunk, controller) { + produced += chunk.length; + if (produced > entry.uncompressedSize) throw new Error(`Unexpected expanded size: ${name}`); + controller.enqueue(chunk); + }, flush() { + if (produced !== entry.uncompressedSize) throw new Error(`Incorrect expanded size: ${name}`); + } }); + await Promise.all([ + entry.getData(stream.writable, { checkSignature: true }), + writer.add(name, stream.readable.pipeThrough(limit), { + versionMadeBy: 0x31e, externalFileAttributes: entry.externalFileAttributes, + onprogress: completed => report({ phase: writer instanceof DirectoryWriter ? "Writing files" : "Compressing ZIP", input: name, completed, total: entry.uncompressedSize }) + }) + ]); +} + +class DirectoryWriter { + constructor(destination) { + this.destination = destination; + } + + async add(name, source, options = {}) { + if (options.directory) { + await this.destination.directory(name); + return; + } + const output = await this.destination.file(name); + if (source instanceof ReadableStream) { + let completed = 0; + const reader = source.getReader(); + const writer = output.getWriter(); + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + for (let offset = 0; offset < value.length; offset += 65536) { + const chunk = value.subarray(offset, offset + 65536); + await writer.write(chunk); + completed += chunk.length; + options.onprogress?.(completed); + } + } + await writer.close(); + } catch (error) { + await reader.cancel(error); + await writer.abort(error); + throw error; + } finally { + reader.releaseLock(); + writer.releaseLock(); + } + } else { + await source.init(); + const writer = output.getWriter(); + try { + for (let offset = 0; offset < source.size; offset += 65536) { + await writer.write(await source.readUint8Array(offset, Math.min(65536, source.size - offset))); + } + await writer.close(); + } catch (error) { + await writer.abort(error); + throw error; + } finally { + writer.releaseLock(); + } + } + } + + async close() {} +} + +export async function packageArchive(archive, converted, outputName, libraryFile, destination, report, directory = false) { + const libraries = new ZipReader(new BoundedBlobReader(libraryFile)); + const writer = directory ? new DirectoryWriter(destination) : new ZipWriter(destination, { level: 9, zip64: true, bufferedWrite: false, useWebWorkers: false, useCompressionStream: false, preventClose: true }); + const names = new Set(); + const claim = name => { + validPath(name); + const key = name.replace(/\/$/u, "").toLowerCase(); + if (names.has(key)) throw new Error(`Output path collision: ${name}`); + names.add(key); + }; + try { + const libraryEntries = await libraries.getEntries(); + if (!libraryEntries.length || libraryEntries.length > 4096) throw new Error("Invalid library archive"); + for (const entry of libraryEntries) { + if (!entry.filename.startsWith("libs/") || entry.encrypted || ((entry.externalFileAttributes >>> 16) & 0xf000) === 0xa000) throw new Error("Invalid library archive layout"); + claim(entry.filename); + } + const replaced = new Set(converted.modules.map(module => module.name)); + const originalFiles = archive.files.filter(item => !replaced.has("app0/" + item.name)); + for (const item of originalFiles) claim("app0/" + item.name); + claim(outputName); + if (converted.diagnostics) claim("windows-diagnostics-imports.txt"); + for (const module of converted.modules) { + if (!module.name.startsWith("app0/sce_module/")) throw new Error("Invalid converted module path"); + claim(module.name); + } + const binary = { versionMadeBy: 0x31e, externalFileAttributes: (0o100755 << 16) >>> 0 }; + await writer.add(outputName, new Uint8ArrayReader(converted.executable), binary); + if (converted.diagnostics) await writer.add("windows-diagnostics-imports.txt", new Uint8ArrayReader(new TextEncoder().encode(converted.diagnostics))); + for (const module of converted.modules) await writer.add(module.name, new Uint8ArrayReader(module.bytes), binary); + for (const entry of libraryEntries) { + if (entry.directory) await writer.add(entry.filename, undefined, { directory: true }); + else await appendEntry(writer, entry.filename, entry, report); + } + for (const { entry, name } of originalFiles) { + if (entry.directory) await writer.add("app0/" + name, undefined, { directory: true }); + else await appendEntry(writer, "app0/" + name, entry, report); + } + await writer.close(); + } finally { + await libraries.close(); + } +} diff --git a/demo/web/index.html b/demo/web/index.html new file mode 100644 index 0000000000..c280516fb8 --- /dev/null +++ b/demo/web/index.html @@ -0,0 +1,29 @@ + + + + + +AnyPS5 · Converter + + +
+
+

LOCAL CONVERSION

PS5 apps to Windows/Linux converter

+ +
+

Convert an app

Files stay on your device
+

Choose a folder or drop a ZIP

One executable ELF, sce_sys/ and sce_module/.
Other app files can stay in the archive.

ZIP & ZIP64 · Decrypted ELF files only

+ + +
Convert for

+
+

Choose a ZIP or folder to get started.

+ + + +
+

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. Larger results require desktop Chrome or Edge with direct disk saving. Executable, modules and icon are limited to 256 MiB combined; use the native relinker for larger conversion inputs.

+

Conversion does not guarantee that an app runs. Check the community compatibility list for known blockers and reports.

+
+ + diff --git a/demo/web/tests/archive.test.mjs b/demo/web/tests/archive.test.mjs new file mode 100644 index 0000000000..4470741f74 --- /dev/null +++ b/demo/web/tests/archive.test.mjs @@ -0,0 +1,141 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; +import path from "node:path"; +import { register } from "node:module"; + +register("./cdn-loader.mjs", import.meta.url, { data: { url: process.env.ANYPS5_TEST_ZIP_URL, path: process.env.ANYPS5_TEST_ZIP_PATH } }); + +const site = name => pathToFileURL(path.join(process.argv[2], name)).href; +const { Archive, memoryEstimate, MEMORY_LIMIT, packageArchive } = await import(site("archive.mjs")); +const { BlobReader, BlobWriter, Uint8ArrayReader, Uint8ArrayWriter, ZipReader, ZipWriter } = await import(process.env.ANYPS5_TEST_ZIP_URL); +const { default: createRelinker } = await import(site("native/relinker_wasm.mjs")); +const native = await createRelinker(); +const executable = new Uint8Array(await readFile(path.join(process.argv[3], "main.elf"))); +const module = new Uint8Array(await readFile(path.join(process.argv[3], "module.elf"))); +const resource = new Uint8Array(2 * 1024 * 1024).fill(65); + +async function zip(entries) { + const output = new BlobWriter("application/zip"); + const writer = new ZipWriter(output, { zip64: true, level: 9, useWebWorkers: false, useCompressionStream: false }); + for (const [name, bytes, options] of entries) await writer.add(name, bytes ? new Uint8ArrayReader(bytes) : undefined, options); + return writer.close(); +} + +const source = await zip([["title/", null, { directory: true }], ["title/program.dat", executable], ["title/sce_module/library.prx.esbak", module], ["title/sce_module/library.prx", new Uint8Array([79, 21, 61, 29])], ["title/sce_module/library.prx.esbak.guest.prx", new Uint8Array([77, 90, 0, 0])], ["title/sce_sys/param.json", new TextEncoder().encode("{}")], ["title/shaders/example.frag", executable], ["title/content/resource.bin", resource]]); +const folder = [ + { path: "title/program.dat", file: new Blob([executable]) }, + { path: "title/sce_module/library.prx.esbak", file: new Blob([module]) }, + { path: "title/sce_module/library.prx", file: new Blob([new Uint8Array([79, 21, 61, 29])]) }, + { path: "title/sce_module/library.prx.esbak.guest.prx", file: new Blob([new Uint8Array([77, 90, 0, 0])]) }, + { path: "title/sce_sys/param.json", file: new Blob(["{}"]) }, + { path: "title/shaders/example.frag", file: new Blob([executable]) }, + { path: "title/content/resource.bin", file: new Blob([resource]) } +]; +for (const [kind, selected] of [["ZIP", source], ["folder", folder]]) { +for (const target of ["windows", "linux"]) { + const archive = new Archive(selected); + const summary = await archive.inspect(() => {}); + assert.equal(summary.name, "program.dat"); + assert.equal(summary.root, "title/"); + assert.equal(summary.modules, 1); + assert.equal(summary.selectedBytes, executable.length + module.length); + const input = await archive.conversionInput(target, false, () => {}); + assert.equal(input.modules.length, 1); + assert.deepEqual(input.executable, executable); + assert.equal(input.modules[0].name, "sce_module/library.prx.esbak"); + const events = []; + const converted = native.Convert(input, event => events.push(event)); + assert.equal(converted.ok, true, converted.error); + assert.equal(converted.executable[0], target === "windows" ? 77 : 127); + if (target === "windows") { + const image = new DataView(converted.executable.buffer, converted.executable.byteOffset, converted.executable.byteLength); + assert.equal(image.getUint16(image.getUint32(60, true) + 24 + 68, true), 3); + } + assert(events.some(event => event.stage === 6 && event.completed > 0)); + const libraries = await zip([["libs/system.prx", new Uint8Array([1, 2, 3])]]); + const chunks = []; + let maxChunk = 0; + const destination = new WritableStream({ async write(chunk) { + maxChunk = Math.max(maxChunk, chunk.length); + chunks.push(chunk.slice()); + await new Promise(resolve => setTimeout(resolve, 1)); + } }); + await packageArchive(archive, converted, input.outputName, libraries, destination, () => {}); + await destination.close(); + const reader = new ZipReader(new BlobReader(new Blob(chunks))); + const entries = await reader.getEntries(); + const byName = new Map(entries.map(entry => [entry.filename, entry])); + assert(byName.has(input.outputName)); + assert(byName.has("libs/system.prx")); + assert.deepEqual(await byName.get("app0/shaders/example.frag").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable); + assert.deepEqual(await byName.get("app0/sce_module/library.prx.esbak.guest.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), converted.modules[0].bytes); + assert.deepEqual(await byName.get("app0/sce_module/library.prx").getData(new Uint8ArrayWriter(), { checkSignature: true }), new Uint8Array([79, 21, 61, 29])); + assert.deepEqual(await byName.get("app0/program.dat").getData(new Uint8ArrayWriter(), { checkSignature: true }), executable); + assert.deepEqual(await byName.get("app0/content/resource.bin").getData(new Uint8ArrayWriter(), { checkSignature: true }), resource); + assert.equal((byName.get(input.outputName).externalFileAttributes >>> 16) & 0o777, 0o755); + assert(maxChunk < resource.length); + const saved = new Map(); + let active = false; + let directoryChunk = 0; + await packageArchive(archive, converted, input.outputName, libraries, { + async directory(name) { saved.set(name, null); }, + async file(name) { + assert.equal(active, false); + active = true; + const pieces = []; + return new WritableStream({ + async write(chunk) { + directoryChunk = Math.max(directoryChunk, chunk.length); + pieces.push(chunk.slice()); + await new Promise(resolve => setTimeout(resolve, 1)); + }, + async close() { + saved.set(name, new Uint8Array(await new Blob(pieces).arrayBuffer())); + active = false; + } + }); + } + }, () => {}, true); + assert.equal(active, false); + assert(directoryChunk < resource.length); + assert.equal(saved.size, entries.length); + for (const entry of entries) { + assert(saved.has(entry.filename)); + if (!entry.directory) assert.deepEqual(saved.get(entry.filename), await entry.getData(new Uint8ArrayWriter(), { checkSignature: true })); + } + await reader.close(); + await archive.close(); + console.log(`${kind}, ${target}: conversion, original files, modules, permissions, streamed ZIP and directory output verified`); +} +} + +for (const [entries, error] of [ + [[["one", executable], ["two", executable]], /exactly one/u], + [[["readme", new Uint8Array([1])]], /exactly one/u], + [[["app", executable], ["..\/escape", resource]], /Unsafe ZIP/u], + [[["app", executable], ["APP", resource]], /Duplicate ZIP/u], + [[["app", executable], ["folder", resource], ["folder/file", resource]], /also used as a folder/u], + [[["app", executable], ["link", resource, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special/u] +]) { + const archive = new Archive(await zip(entries)); + await assert.rejects(archive.inspect(() => {}), error); + await archive.close(); +} +const missing = new Archive(await zip([["any-name", executable]])); +assert.equal((await missing.inspect(() => {})).warnings.length, 2); +await missing.close(); +assert(memoryEstimate(200 * 1024 ** 3, 1024, 1024, 100) > MEMORY_LIMIT); +const invalid = native.Convert({ target: "unsupported" }, () => {}); +assert.equal(invalid.ok, false); +console.log("Archive validation, missing-folder warnings and large-download budget verified"); + +const workerMessages = []; +globalThis.self = { postMessage: message => workerMessages.push(message) }; +await import(site("worker.mjs")); +await self.onmessage({ data: { type: "convert", target: "windows", mode: "disk" } }); +assert.equal(workerMessages.length, 1); +assert.equal(workerMessages[0].type, "error"); +assert.match(workerMessages[0].message, /browsers block DLL creation/u); +delete globalThis.self; +console.log("Windows folder output rejected before conversion or file writes"); diff --git a/demo/web/tests/cdn-loader.mjs b/demo/web/tests/cdn-loader.mjs new file mode 100644 index 0000000000..13bc480b2f --- /dev/null +++ b/demo/web/tests/cdn-loader.mjs @@ -0,0 +1,9 @@ +let library; + +export function initialize(data) { + library = data; +} + +export function resolve(specifier, context, nextResolve) { + return nextResolve(specifier === library.url ? library.path : specifier, context); +} diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs new file mode 100644 index 0000000000..9a0017792f --- /dev/null +++ b/demo/web/worker.mjs @@ -0,0 +1,115 @@ +import { Archive, MEMORY_LIMIT, memoryEstimate, packageArchive } from "./archive.mjs"; + +let archive; +let busy = false; +let pendingWrite; +let lastReport = 0; +let lastPhase = ""; + +function report(event) { + const phase = event.phase ?? String(event.stage); + const now = performance.now(); + if (!event.message && phase === lastPhase && event.completed !== event.total && now - lastReport < 80) return; + lastReport = now; + lastPhase = phase; + self.postMessage({ type: "progress", ...event }); +} + +async function downloadLibraries(target) { + report({ phase: "Downloading system libraries" }); + const response = await fetch("./release.json", { cache: "no-cache" }); + if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); + const release = await response.json(); + const asset = release.platforms[target]; + if (!asset || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); + if (!latestResponse.ok) throw new Error(`Cannot verify the latest release (${latestResponse.status}). Try again later.`); + const latest = await latestResponse.json(); + if (latest.tag_name !== release.tag) throw new Error("A new release is being published to this site. Try again after the Pages build finishes."); + const download = await fetch(asset.path); + if (!download.ok) throw new Error(`Cannot download system libraries (${download.status})`); + const bytes = await download.arrayBuffer(); + if (bytes.byteLength !== asset.size) throw new Error("System library download is incomplete"); + const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); + if (digest !== asset.sha256) throw new Error("System library checksum mismatch"); + return { file: new Blob([bytes]), release, expandedBytes: asset.expandedBytes }; +} + +self.onmessage = async ({ data }) => { + if (data.type === "written") { + if (!pendingWrite) return; + const resolve = pendingWrite; + pendingWrite = null; + resolve(); + return; + } + if (busy) return; + busy = true; + try { + if (data.type === "inspect") { + if (archive) await archive.close(); + archive = new Archive(data.files ?? data.file); + const summary = await archive.inspect(report); + self.postMessage({ type: "inspected", summary }); + } else if (data.type === "convert") { + if (data.target === "windows" && data.mode === "disk") throw new Error("Windows folder output is unavailable because browsers block DLL creation. Choose a ZIP save mode."); + if (!archive) throw new Error("Select a ZIP or folder first"); + const libraries = await downloadLibraries(data.target); + const estimate = memoryEstimate(archive.totalBytes, archive.selectedBytes, libraries.expandedBytes, archive.entries.length); + if (data.mode === "memory" && estimate > MEMORY_LIMIT) throw new Error("This result exceeds the 256 MiB memory-download budget. Use Save ZIP to disk in desktop Chrome or Edge."); + report({ phase: "Loading converter" }); + const { default: createRelinker } = await import("./native/relinker_wasm.mjs"); + const native = await createRelinker(); + let input = await archive.conversionInput(data.target, data.toIntel, report); + const outputName = input.outputName; + const converted = native.Convert(input, report); + input = null; + if (!converted.ok) throw new Error(converted.error); + const chunks = []; + let size = 0; + const send = (event, transfer = []) => new Promise(resolve => { + pendingWrite = resolve; + self.postMessage(event, transfer); + }); + if (data.mode === "disk") { + const destination = { + directory: name => send({ type: "directory", name }), + async file(name) { + await send({ type: "open-file", name }); + return new WritableStream({ + async write(chunk) { + size += chunk.byteLength; + const bytes = chunk.slice(); + await send({ type: "write", bytes }, [bytes.buffer]); + }, + close: () => send({ type: "close-file" }) + }); + } + }; + await packageArchive(archive, converted, outputName, libraries.file, destination, report, true); + } else if (data.mode === "memory" || data.mode === "zip") { + const destination = new WritableStream({ + async write(chunk) { + size += chunk.byteLength; + if (data.mode === "zip") { + const bytes = chunk.slice(); + await send({ type: "write", bytes }, [bytes.buffer]); + return; + } + if (size > MEMORY_LIMIT) throw new Error("The compressed ZIP exceeded 256 MiB. Use Save ZIP to disk."); + chunks.push(chunk.slice()); + } + }); + await packageArchive(archive, converted, outputName, libraries.file, destination, report); + await destination.close(); + } else throw new Error("Unknown save mode"); + self.postMessage({ type: "complete", mode: data.mode, blob: data.mode === "memory" ? new Blob(chunks, { type: "application/zip" }) : null, outputName, size, release: libraries.release.tag }); + await archive.close(); + archive = null; + } else throw new Error("Unknown worker request"); + } catch (error) { + self.postMessage({ type: "error", message: error instanceof Error ? error.message : String(error) }); + } finally { + busy = false; + } +}; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 7ae815525b..5227da96e6 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -104,6 +104,7 @@ flowchart LR - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. - A 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`). ### Untracked storage image comparisons diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index d8c4b9d638..82b5576701 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -27,6 +27,7 @@ git submodule update --init --recursive ## Requirements - 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. - 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. @@ -62,6 +63,7 @@ Project switches accept `ON` or `OFF`: | `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | +| `-DAPS5_ENABLE_TRACY=ON` | `OFF` | Build the [Tracy](#tracy) profiler client into the agc driver. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | | `-DAGC_BUILD_VISUAL_TEST=ON` | `OFF` | Build the standalone AGC SPIR-V visual test. | @@ -92,6 +94,12 @@ ctest --test-dir build/write-watch --output-on-failure With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them. +## Tracy + +`-DAPS5_ENABLE_TRACY=ON` builds the [Tracy](https://github.com/wolfpld/tracy) 0.14.1 client from `3rdparty/tracy` as a shared library next to the patched `.prx` files (`libTracyClient.dll`, `libTracyClient.so.0.14.1` on Linux); copy it with them. Every `PerformanceTimer` scope of libSceAgcDriver and libSceVideoOut is a zone, every presented flip ends a frame, and the queue workers and the presenter are named threads. Connect the Tracy 0.14.1 profiler to `127.0.0.1`: the client listens only on localhost and records only while a profiler is connected. Sampling, system tracing, call stacks, code transfer and Tracy's crash handler are disabled. Without the flag nothing of Tracy is built. + +On Windows MinGW frees `thread_local` objects before their destructors run, so the Tracy client is built without `thread_local` destructors and never destroys its event queue: a thread that emitted a zone and ended keeps its event blocks (2 MiB each) until the process exits. + ## Compute pipeline replay Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. @@ -114,6 +122,26 @@ Each invocation creates one pipeline. With no `--cache-in`, its application pipe Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default. +## Worker sampler + +Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. + +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. + +## Performance trace + +Set `APS5_TRACE_FRAMES=:` to record `` presents from present `` (counted from 0), or `APS5_TRACE_SECONDS=:` to record from the first present at least `` seconds after the first present until the first present `` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. + +Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. Each thread allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording and sets `trace_dropped`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. + +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped` and `trace`. A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. + +The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). + +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. + ## Driver thread priority Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal. diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index ca20cad090..8b532eeb62 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -12,6 +12,10 @@ For these wrappers, the JSON group adds `shared_sources` with the source library A library with no scanned definitions and no shared sources, such as one whose `Export.cpp` only has `APS5_DUMMY_FUN` because its functions are implemented in another library, is listed as having no exports instead of `0/0`. It is left out of the map. +## History + +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` (shown in the README) and `history.html` (the chart with a tooltip per day plus a table, linked from the end of the progress page). It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 31 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points (and their tooltips) are drawn up to 45 days, after which the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. + ## Comparisons Comparisons identify functions by library and name. When a head group becomes a shared-source wrapper, its previous definitions are compared under the source library's identity. Removing a duplicate implementation is reported as sharing sources, without reporting the retained functions as removed. @@ -25,6 +29,7 @@ Implemented definitions are also classified as behaviourally empty when their bo ```sh python3 tools/tests/test_progress.py python3 tools/progress.py /tmp/anyps5-progress +python3 tools/tests/test_progress_history.py ``` The regressions check that adding test callbacks leaves counts and comparisons unchanged, including when a callback shares a pending function's name or a wrapper uses shared sources. They also create a source library and a copied wrapper, move the wrapper to a shared builder, check output labels and totals, then verify real regressions and removals are still reported. With `BUILD_TESTING=ON` and Python available, CTest registers them as `progress_report`. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3a1bae53f8..1a7c6bbe89 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,6 +4,7 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build +- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll @@ -21,6 +22,7 @@ Throughout the project, every function at every stage either **does exactly what ### Unknown function info - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures +- `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 diff --git a/docs/dev/debt/libSceAcm.md b/docs/dev/debt/libSceAcm.md index bb0c47be75..c5c9b90f30 100644 --- a/docs/dev/debt/libSceAcm.md +++ b/docs/dev/debt/libSceAcm.md @@ -3,3 +3,8 @@ ### Silent stubs - [libSceAcm](../../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. + +### Unknown function info + +- PPSA02530 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceAcmBatchJobPriority](../../../core/libs/prx/libSceAcm/Export.cpp) (libSceAcm) and [scePadVrControllerGetTriggerEffectStates](../../../core/libs/prx/libScePad/Export.cpp) (libScePad). + diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 5cdd7c2b12..cf540b7c52 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,8 +7,10 @@ - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws - [Command buffer cursors](../../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [sceAgcGetRegisterDefaultsInternal](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - unsupported register-default versions return `0x80890001` following [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c); AnyPS5 keeps version 13 support because its register-default tables include it, while that reference accepts only versions 0–12. The upper version accepted by PS5 firmware is unverified - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [sceAgcGetGsOversubscription](../../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws +- [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access - [sceAgcCbCondWrite](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown @@ -32,5 +34,7 @@ ### Functional - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents +- [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. - [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 diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0a8e13c845..a5d4bcb316 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,6 +2,7 @@ ### Unknown function info +- [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 @@ -14,21 +15,29 @@ - [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. -- [Shader registration](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown +- [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown - [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. - [Depth-only and stencil HTILE fill](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - in a whole-HTILE fill, ZMask (bits 0-3) 0 clears the depth and SMem (bits 8-9) 0 clears the stencil, while SMem 3 leaves the stencil alone (`HtileFillClears`); other SMem values are taken to leave it alone too. The bit meanings follow the gfx10 HTILE layout as the code reads it, not a measurement; `agc_driver_depth_fast_clear_submit` pins them for the paths the Driver takes. - [HTILE address of a depth array slice](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `decodeDepth` adds the `DB_DEPTH_VIEW` slice offset to the Z and stencil addresses but not to `htileAddress`, so every slice of a layered depth target shares slice 0's HTILE address and a fill of slice 0's HTILE marks all slices cleared. Astro's Playroom fills the HTILE of each of its three shadow cascades 0x40000 apart (0x24000 bytes of tiles each) back to back before drawing, so the result is still right there; a title that clears one slice between draws to another would lose depth. The per-slice HTILE stride is not derived yet. +- [sceAgcDriverGetHsOffchipParam](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver, NID `r28hEh6cNH0`) - declared as a throwing stub. The two `uint16_t` output pointers and integer return type follow [ps5-vulkan](https://github.com/blackbearreloaded/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/native/tess_driver_import_stub.c#L10), which reads the pair before it binds a tessellation factor ring. The meaning of the two values, their initial state and how they relate to `sceAgcDriverSetHsOffchipParam`, which keeps nothing here, are unknown; the stub does not write caller memory. PPSA99011 imports it +- [Vertex input and color target overlap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (AGC driver) - direct draws exclude the descriptor prefix skipped by `firstVertex` or `firstInstance` from the alias check; indexed draws conservatively use index zero as the lower bound before adding `firstVertex`. Indirect draws still check the full descriptor extent. Reads of the active target's bytes remain unsupported; same-draw console ordering is not established. ### 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. Matching depth/stencil storage access still throws. +- [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 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. +- [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_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. - [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. +- [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline @@ -37,8 +46,9 @@ - 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. -- [Storage-image units under a Linux import window](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. 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. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. +- [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. - [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. @@ -60,29 +70,33 @@ - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT`, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout, `STENCIL_OP_VAL_EXPORT_ENABLE` and a device without `VK_EXT_shader_stencil_export` (NVIDIA's driver 610.57 offers none). +- [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, codes other than 0 and 0xF, a cleared tile of a DCC target and a fast clear of a mipmapped, 3D or array color target throw. When the clear runs as a compute pass (a resident target whose CMASK is host-imported), an invalid code throws at the next fast-clear materialization, not at the one that read it -- [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. -- [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, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them 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, 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 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 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. 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. - [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 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) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [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. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [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. - [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/Draw.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) is expanded only by a CB DCC decompress pass over uniform keys of a non-linear target: every texel takes 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. A fast-clear eliminate over such keys and mixed keys throw; textures and color targets that read them use their stored texels. "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 +- [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [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. - [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) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. @@ -97,3 +111,5 @@ - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [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. diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 237e7028e5..db01df9a8b 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -3,11 +3,14 @@ ### Unknown function info - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order +- [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. - [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded ### Functional +- [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. - [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. - [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known +- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 8830908bf0..1c9129423e 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -4,8 +4,9 @@ - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB - [sceAppContentGetAddcontDownloadProgress](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes +- [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` as `sceAppContentTemporaryDataFormat` does, and another mount point throws as in `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified ### 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 +- [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 - [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 176d82c4d0..7d7b5dfcc7 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,9 +2,9 @@ ### Unknown function info -- PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list -- PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list -- [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to +- [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list +- [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to +- [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted @@ -15,9 +15,11 @@ - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented +- [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws ### 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. - [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/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index 1d9feb3e08..e771fdeec7 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) - imported by PPSA06465, which fails to load without it; added without an implementation because its signature is unknown diff --git a/docs/dev/debt/libSceContentSearch.md b/docs/dev/debt/libSceContentSearch.md new file mode 100644 index 0000000000..2222020f2e --- /dev/null +++ b/docs/dev/debt/libSceContentSearch.md @@ -0,0 +1,5 @@ +# libSceContentSearch technical debt + +### Functional + +- [libSceContentSearch](../../../core/libs/prx/libSceContentSearch/Export.cpp) searches a content library that is empty on the host, as nothing is exported to it. The signatures follow [AlexAltea/orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L555-L569) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentSearch.cs#L255-L316), which agree; the filter and sort layouts and enums, the limit of 92 rows, `0x809D100A` for a larger limit and `0x809D1001` before `sceContentSearchInit` are SharpProspero's. `sceContentSearchGetNumOfContent`, `sceContentSearchGetTotalContentSize` and `sceContentSearchSearchContent` report 0 rows and 0 bytes without writing a content row, and the update id they and `sceContentSearchGetContentLastUpdateId` report is always 0; no reference gives the console's update id for a library that never changed. Filters and sort conditions are only checked against SharpProspero's enums, and a null output, a null array with a non-zero length, an empty column set, a null filter value or an unknown enum value throws, as the console's errors for them are unknown; a null column set with length 0 is taken as no filter. `sceContentSearchOpenMetadata` and `sceContentSearchOpenMetadataByContentId` throw, as no reference gives the error for a path or content id that is not in the library, so no metadata id is ever handed out and `sceContentSearchCloseMetadata`, `sceContentSearchGetMetadataFieldInfo` and `sceContentSearchGetMetadataValue` throw. Every title is taken to hold the content-search permission (SharpProspero's `0x809D1013` is never returned). The order of the initialization, argument and limit checks is unverified diff --git a/docs/dev/debt/libSceDbgThreadSanitizer.md b/docs/dev/debt/libSceDbgThreadSanitizer.md new file mode 100644 index 0000000000..5dc8905e10 --- /dev/null +++ b/docs/dev/debt/libSceDbgThreadSanitizer.md @@ -0,0 +1,5 @@ +# libSceDbgThreadSanitizer technical debt + +### Unknown function info + +- [__tsan_acquire, __tsan_release](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) (libSceDbgThreadSanitizer, NIDs `XyX1104ACcU` and `IkoI44e0OXc`) - declared as throwing stubs: AnyPS5 runs no race detector. The `void (void* addr)` signatures are those of LLVM compiler-rt's [tsan_interface.h](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/compiler-rt/include/sanitizer/tsan_interface.h#L24-L25), whose names hash to the imported NIDs; that the console library has the same signatures is assumed. PPSA23226's bundled `libSceJobManager_nosubmission.prx` imports both and calls them only after [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) reports the sanitizer enabled, which it never does here diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 64d7e9b47e..aab10875a0 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,6 +6,7 @@ ### Unknown function info +- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero - PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 72914c6312..61011254e4 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -8,5 +8,5 @@ ### Functional -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) and [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 13577a0638..0a303516f1 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -10,4 +10,7 @@ - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) follows FreeBSD 11's `lib/libc/amd64/gen/makecontext.c`: at most six arguments, a stack of at least `MINSIGSTKSZ` (2048) bytes, and `exit(0)` when the function returns with no `uc_link`. The console's version is not verified - [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [Thread exit destructors](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped +- [fdopendir](../../../core/libs/prx/libSceLibcInternal/Directory.cpp) (libSceLibcInternal) - the directory stream starts at the directory's first entry, while FreeBSD's [`fdopendir`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/opendir.c#L67-L74) reads from the descriptor's current offset, and `FD_CLOEXEC` is not set on the descriptor. As in FreeBSD, a descriptor that is not open fails with `EBADF`, one that is not a directory with `ENOTDIR` (both leave it open), and `closedir` closes it - [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 diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 08c35423d4..95103ae7a7 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -2,7 +2,7 @@ ### Unknown function info -- PPSA01530, PPSA23827 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), [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), 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. +- 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) - [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters @@ -10,3 +10,5 @@ - [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` - [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 +- [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/libSceNetCtlApDialog.md b/docs/dev/debt/libSceNetCtlApDialog.md new file mode 100644 index 0000000000..aefc800699 --- /dev/null +++ b/docs/dev/debt/libSceNetCtlApDialog.md @@ -0,0 +1,5 @@ +# libSceNetCtlApDialog technical debt + +### Silent stubs + +- [libSceNetCtlApDialog](../../../core/libs/prx/libSceNetCtlApDialog/Export.cpp) - the access-point dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled, as no access-point connection is brought up. `Open` takes the 104-byte parameter block and `GetResult` writes a 40-byte result (int32 result and 36 zeroed reserved bytes), following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/NetCtlApDialog.cs); the parameter block is not validated and the 0x80B8xxxx error codes follow the other common dialogs, neither verified on hardware. Deathloop imports it diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index b1d9e08728..1826329e43 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -22,4 +22,6 @@ - [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. `sceNgs2GeomCalcListener` and `sceNgs2GeomApply` stay unimplemented +- [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/libSceNpAuth.md b/docs/dev/debt/libSceNpAuth.md index d7ae48d68b..8e344dfda7 100644 --- a/docs/dev/debt/libSceNpAuth.md +++ b/docs/dev/debt/libSceNpAuth.md @@ -2,4 +2,6 @@ ### Unknown function info -- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it and fails to load without it. The request parameter and code output are not known, and there is no PSN sign-in for it to succeed against. +- [sceNpAuthGetAuthorizedAppCode](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - declared without parameters and returns `SCE_NP_ERROR_SIGNED_OUT` without reading them, as there is no PSN sign-in; the request parameter and code output are not known. Minecraft (PPSA17221) imports it, fails to load without it, and after its sign-in dialog treats the signed-out code as offline play. +- [sceNpAuthAbortRequest, sceNpAuthGetAuthorizationCodeV3, sceNpAuthGetIdTokenV3, sceNpAuthPollAsync, sceNpAuthWaitAsync](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - request tracking, `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` (0x80550306) on unknown request IDs, `ORBIS_NP_AUTH_ERROR_INVALID_ID` (0x80550307) on non-async or unprocessed requests in Poll/Wait, `ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT` (0x80550301) on null arguments and repeated calls on completed requests, and `ORBIS_NP_AUTH_ERROR_ABORTED` (0x80550304) after an abort follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4); async requests return OK and defer `SCE_NP_ERROR_SIGNED_OUT` to `sceNpAuthPollAsync`/`sceNpAuthWaitAsync` while sync requests fail immediately; PlayStation Network is not contacted. The parameter `size` fields are not checked (`ORBIS_NP_AUTH_ERROR_INVALID_SIZE`, structure layouts unknown), the request table has no limit (`ORBIS_NP_AUTH_ERROR_REQUEST_MAX`), and console behavior on PS5 is unverified +- [sceNpAuthSetTimeout](../../../core/libs/prx/libSceNpAuth/Export.cpp) (libSceNpAuth) - imported by PPSA01317, which fails to load without it. The signature `(reqId, resolveRetry, resolveTimeout, connTimeout, sendTimeout, recvTimeout)` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_auth.cpp) (PS4), which only stubs it. An unknown request ID returns `ORBIS_NP_AUTH_ERROR_REQUEST_NOT_FOUND` like the other request calls; otherwise it returns OK and keeps nothing, as no request reaches the network the timeouts would bound. Argument checks of the console are unverified diff --git a/docs/dev/debt/libSceNpEntitlementAccess.md b/docs/dev/debt/libSceNpEntitlementAccess.md index 51225037b3..b457b050c8 100644 --- a/docs/dev/debt/libSceNpEntitlementAccess.md +++ b/docs/dev/debt/libSceNpEntitlementAccess.md @@ -4,4 +4,7 @@ - [sceNpEntitlementAccessGetEntitlementKey](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified - [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified -- PPSA08427 imports declared without parameters, signatures unknown: [sceNpEntitlementAccessGetGameTrialsFlag](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), imported by its bundled `pros.sdk.Prospero.prx` and not called before the main menu. Name from the aerolib NID list + +### Functional + +- [sceNpEntitlementAccessGetGameTrialsFlag, sceNpEntitlementAccessGetPftFlag](../../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) (libSceNpEntitlementAccess) - reports a full game, not a game trial or Play First Trial: writes 0 to the `uint32_t` output and returns 0, and a null pointer returns `0x817D0002`. The single-pointer signature, the value, the one-word write and the error of `sceNpEntitlementAccessGetGameTrialsFlag` follow [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Np/NpEntitlementAccessExports.cs#L98-L116), which has no `sceNpEntitlementAccessGetPftFlag`; those of `sceNpEntitlementAccessGetPftFlag` follow [dlc_emu](https://github.com/drakmor/dlc_emu/blob/main/src/dlc_modules/dlc_content.cpp), which takes the same `SceNpEntitlementAccessGameTrialsFlag*` as the trials flag, writes the OFF value 0 and returns the parameter error for null; `0x817D0002` is the parameter error of KytyPS5, prosper and `sceNpEntitlementAccessGetEntitlementKey`, while `sceNpEntitlementAccessGetSkuFlag` here returns `0x80558003`. None of this is a console measurement, and PRAGMATA (PPSA02530) shows four live registers at the call, so further arguments are not ruled out diff --git a/docs/dev/debt/libSceNpPartner001.md b/docs/dev/debt/libSceNpPartner001.md index f032f97dab..a8a1d8b242 100644 --- a/docs/dev/debt/libSceNpPartner001.md +++ b/docs/dev/debt/libSceNpPartner001.md @@ -1,5 +1,6 @@ # libSceNpPartner001 technical debt -### Unknown function info +### Functional -- [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - names and the no-argument signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); imported by PPSA23566, not confirmed on a PS5 title +- [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - initialization marks the library initialized without contacting the server and termination clears it, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); whether the console fails initialization while signed out is unknown. Termination without initialization throws where shadPS4 returns the PS4 error `0x819D0001`, whose PS5 value is unverified, and a second initialization throws where shadPS4 returns 0 without checking. PPSA23566 imports both, not confirmed on its start-up path +- [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - initialization marks the library initialized without contacting the server and termination clears it, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); whether the console fails initialization while signed out is unknown. Termination without initialization throws where shadPS4 returns the PS4 error `0x819D0001`, whose PS5 value is unverified, and a second initialization throws where shadPS4 returns 0 without checking. PPSA23566 imports both, not confirmed on its start-up path diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 907ab68a6d..9f9b7ad514 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -5,3 +5,4 @@ - [sceNpSessionSignalingGetConnectionFromPeerAddress](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature - [sceNpSessionSignalingGetMemoryInfo](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature +- [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index d42b614ee2..148b2634e6 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -4,3 +4,4 @@ - [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. - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws +- [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 975723bfc5..8cfd5341d6 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,7 @@ ### Unknown function info +- [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown - [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 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/libSceProprietaryVoiceChatHelperPrx.md b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md new file mode 100644 index 0000000000..027e2ab7dc --- /dev/null +++ b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md @@ -0,0 +1,5 @@ +# libSceProprietaryVoiceChatHelperPrx technical debt + +### Silent stubs + +- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throws, because nothing it could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 476e359f81..9dd2ab63c4 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -6,3 +6,4 @@ - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signatures, imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`; declared without parameters. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) +- [NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw`](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K, exported as `scePsmlUnknown01` and `scePsmlUnknown02`) - imported by PPSA01317, which fails to load without them; names and signatures unknown (not in the aerolib NID list), so they throw diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 74dd2dc4f3..0c42c424a8 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -3,6 +3,7 @@ ### Silent stubs - [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 +- [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified ### Unknown function info diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index ec14acea38..868932aa03 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -3,5 +3,6 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console's zone name table is only known from the names measured on PS5 firmware 13.60 and 13.42; any other name is assumed to take the military letter offset of its first character, as every measured name outside the table does (`HST`, `MSK`, `XYZ`), so a name the console's table holds but nobody measured gets the wrong offset +- [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements on other firmware and title contexts are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index cd0b5ce0b0..45da9c14c8 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -5,6 +5,12 @@ - [sceSaveDataUnknown_RvID8BVeg](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [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 - [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 + +- [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. + diff --git a/docs/dev/debt/libSceShare.md b/docs/dev/debt/libSceShare.md index b203fe8e1c..8ad04d2333 100644 --- a/docs/dev/debt/libSceShare.md +++ b/docs/dev/debt/libSceShare.md @@ -5,3 +5,7 @@ - [sceShareGetCurrentStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceShareGetRunningStatus](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - only the first 32-bit word of the status is known: PPSA01317 treats bits 0x3c of it as a running share feature and gives the call a 40-byte buffer. The host never records or broadcasts, so it stores 0 in that word and leaves the rest untouched; a null status returns `INVALID_PARAM` by analogy with `sceShareGetCurrentStatus` - [sceShareSetContentParamForApplicationTitle](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Unity plugins; the signature follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libShare.cpp), not confirmed on a PS5 title. The application title is only checked for null, as KytyPS5 does, since nothing is captured + +### Functional + +- [sceShareCaptureVideoClipExtended](../../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - imported by PPSA12544's Share plugin; the signature (extended parameter, request id) and the answer follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/147188b143f67c27d8da396e759c19336f64c105/src/libs/libShare.cpp), not confirmed on a PS5 title. Nothing is captured, so, like `sceShareCaptureScreenshotExtended`, it stores the invalid request id -1 when the pointer is not null and returns `NOT_SUPPORTED` without reading the extended parameter, whose layout is unknown diff --git a/docs/dev/debt/libSceSharePlay.md b/docs/dev/debt/libSceSharePlay.md index 5c286e87ca..e6c2086083 100644 --- a/docs/dev/debt/libSceSharePlay.md +++ b/docs/dev/debt/libSceSharePlay.md @@ -7,3 +7,4 @@ ### Functional - [sceSharePlayGetCurrentConnectionInfoA](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceSharePlayProhibitController](../../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - accepts 0 (allow) and 1 (prohibit) and returns 0, following [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Share/SharePlayExports.cs#L9-L31). The setting is not stored, as no Share Play session or visitor controller exists and no export reads it back. sharpemu rejects any other value with its synthetic `0x80020003`, not a Share Play code, so other values throw. Like the other exports here it does not require `sceSharePlayInitialize`. The `int` argument type and the console's error for other values are unverified diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 6f5759fbc7..0a72ff2bfa 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -5,7 +5,8 @@ - [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 ### Functional -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, 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: [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) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 9ff34f9bab..17773650c4 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -4,6 +4,8 @@ - [libSceSystemService](../../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead +- [sceSystemServiceShowControllerSettings](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without showing the controller settings: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it +- [sceSystemServiceOpenChallengeActivity](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - returns success without opening the challenge activity: AnyPS5 has no system UI to open. Cat Quest III (PPSA15065) imports it ### Unknown function info @@ -14,4 +16,5 @@ ### Functional +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the new executable gets the argument list as its command line, after its own path (`CreateProcessW` on Windows, `execv` on Linux), and the old process ends without running host destructors and without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. What the console puts in `argv[0]` is not known; here it is the host path. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 3f407755d1..e1fb5ff21a 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -3,7 +3,8 @@ ### Unknown function info - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware -- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. +- [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. +- [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified ### Functional diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index 3d9fcf8e3e..85e8ca7ba8 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceUsbd](../../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The device and handle calls take the argument lists of FreeBSD 11.4 libusb ([libusb.h](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h)) and return `0x80240002` for the null arguments it rejects with `LIBUSB_ERROR_INVALID_PARAM` ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)); a null device or handle in the calls it treats as null-safe returns 0, null or nothing, as there. `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which libusb dereferences. Any other device or handle was never returned by this library, so those calls throw. The layout, the argument lists and the error codes are unverified on PS5 +- [libSceUsbd](../../../core/libs/prx/libSceUsbd/Export.cpp) passes the calls through to the host with [libusb](https://github.com/libusb/libusb) built from the `3rdparty/libusb-cmake` submodule. Devices, handles and descriptors are libusb's own objects: the device, config, interface and endpoint descriptor layouts of [FreeBSD 11.4 libusb](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h) match libusb's field for field, which `static_assert` pins. Each one handed to the title is tracked, and a pointer this library did not return throws. Errors are libusb's codes mapped to `0x80240000 - code`, with `LIBUSB_ERROR_OTHER` at `0x802400FF`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.cpp); the null arguments FreeBSD libusb rejects ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)) return `0x80240002`, and `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which it dereferences. A submitted transfer is copied into a host transfer whose completion copies the status, actual length and iso packet results back and then calls the title's callback from `sceUsbdHandleEventsTimeout`; transfer flags other than short-not-ok and add-zero-packet throw. `sceUsbdAttachKernelDriver` returns 0 as FreeBSD's stub does, `sceUsbdCheckConnected` reports a device gone when reading its configuration fails with `LIBUSB_ERROR_NO_DEVICE` (FreeBSD asks the kernel directly), and `sceUsbdEventHandlingOk` returns 1. When the host has no USB access, initialization keeps the empty device list and event handling waits for its timeout. Opening, descriptors, string and control requests, interface claims and an asynchronous control transfer were checked against an Xbox 360 controller on Windows; the transfer layout, the argument lists, the error codes and isochronous transfers are unverified on PS5 diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index e2de5493e8..a20f0876af 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -8,11 +8,12 @@ ### 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. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels - [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 @@ -21,3 +22,4 @@ ### Functional - [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. diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index dfc540a6d5..0a6b4a8324 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -5,9 +5,11 @@ - [_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. +- [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. +- [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws -- [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature +- [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` - [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)`) @@ -30,9 +32,12 @@ - [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 - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. +- [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [lgamma](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns the host's `lgamma` but does not store the sign of Γ(x) in the guest's `signgam`, as FreeBSD 11's [`e_lgamma.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma.c) does through `__ieee754_lgamma_r`, because no guest `signgam` is exported. PPSA99004 imports it. Console behaviour is unverified - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code +- [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; @@ -40,19 +45,25 @@ - [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 +- The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter -- [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, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [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. -- [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; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does. Unknown conversions, repeated modifiers and incomplete conversions emit literals as in FreeBSD 11; recognized conversions otherwise go to the host's `strftime`. On Windows the `%-`, `%_` and `%0` padding flags still produce nothing, including on the five expanded conversions (`%-k`, `%_k` and `%0k` go to the host), and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%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. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. -- [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`, and errno is preserved. `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. `openlog` and `closelog` are not implemented, so their identity and options are not applied +- [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)) -- [_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 +- [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), [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 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. +- [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 132b6b9b42..2c6dd4e1bc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -1,15 +1,13 @@ # libkernel technical debt -### Build - -- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. Guest rwlocks ([Pthread.hpp](../../../core/libs/prx/libkernel/Pthread/include/Pthread.hpp)) and the host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. - ### Silent stubs - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutexattr.c) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority), but mutexes do not apply them: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified +- [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) +- [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) returns 0, as `sceKernelIsAddressSanitizerEnabled` does: a converted title never runs under the thread sanitizer, so a title that checks it skips the [libSceDbgThreadSanitizer](../../../core/libs/prx/libSceDbgThreadSanitizer/Export.cpp) annotation calls, which throw. PPSA23226's bundled `libSceJobManager_nosubmission.prx` makes those calls only when it reports nonzero ### Unknown function info @@ -26,17 +24,18 @@ - [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` - [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. `sceAmprAmmGetVirtualAddressRanges` throws for a null output +- [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 - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths -- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures -- [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature +- [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 - [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)) - [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 @@ -50,21 +49,23 @@ - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it -- [sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is the SDK 9 `libc.prx` heap (Astro Bot PPSA21564, PPSA22520), which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0 +- [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in Astro Bot PPSA21564, through `sceKernelMapFlexibleMemory` in Animal Well PPSA22520 01.000.011 - [sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints - [_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. Other descriptors that are not libSceNet sockets throw. Dungeons (PPSA16064) selects on descriptor 0 +- [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 - [__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, 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` and `sys_setreuid`, id 0 (or -1 for an unchanged `setreuid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, 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 - [pthread_barrierattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw -- [sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature -- [sceCoredumpAttachMemoryRegion](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature +- [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported +- [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: 007 First Light (PPSA11386) imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) +- [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either - [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature @@ -84,27 +85,33 @@ - [pnz21I6VjDs](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [qk043QPsrZU](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature -- [sceKernelGetCpuTemperature](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PPSA99902, which fails to load without it; added without an implementation because its behavior is unverified. +- [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [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` ignores the policy and does not check the priority, where the console rejects `SCHED_OTHER` and priorities below 256 with `EINVAL` +- [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 ### 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 - [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. -- [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` 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 +- [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 - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails +- [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies -- [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws +- [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a 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`) @@ -118,36 +125,51 @@ - [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. 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. 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 -- [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` between a guest socket and a host descriptor in either direction, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates and the descriptor numbers come from the guest socket counter, not the lowest free number as in FreeBSD `kern_dup`. The errno values for descriptor exhaustion are not verified against the console. +- [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`) - [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. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [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. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `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 -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [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` +- [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 +- [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 -- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` -- [pause, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throw, as `sigtimedwait` does: guest signals are never left pending, so `sigwait` and `sigwaitinfo` have nothing to take, and `pause` would wait for a handler that `raise`, `kill` and `pthread_kill` only run on the calling thread. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the throw at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)) -- [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run -- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not -- [kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, 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 +- [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 +- [mlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. On main, XPSemu (PPSA97358) fails to load at this import; its `MCL_CURRENT | MCL_FUTURE` calls execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing +- [pause, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). None of the four waits is a cancellation point +- [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it +- [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating +- [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 and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_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 - [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 throws, since guest signals are not delivered to a chosen thread +- [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. - [_fstatfs](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [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. +- [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 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 +- [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/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 9a3300d074..8fb1dba028 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -4,5 +4,12 @@ - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link throws, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified +- [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified +- [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified +- [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask +- [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index eac2ad9fce..995c10223c 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -5,7 +5,7 @@ - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - [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 the console when launched, which is inconvenient for playability. +- 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). - [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. - 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. @@ -15,3 +15,4 @@ - 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. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). +- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1fd56400a0..7163d55302 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -1,17 +1,25 @@ # shader technical debt +### Build + +- winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. + ### Unknown function info -- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select, move or phi is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 -- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused. Bilinear cube comparisons of float, unorm and snorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [Packed pixel ancillary word](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places +- [Color comparison descriptor forms](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused, except on those 8-bit formats, where every texel comparison is 0 (not measured with an anisotropic S#). Bilinear cube comparisons of float, snorm and the other unorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB or UNORM bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [s_setkill](../../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified - [s_sethalt](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws - [s_subvector_loop_begin and s_subvector_loop_end](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [Vertex entry merged wave info](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. On a device whose `subgroupSupportedStages` lacks the vertex stage (lavapipe, MoltenVK), vertex and local programs run as one-lane waves: `s3` is `1 | 1 << 8`, the lane id is 0, a ballot is the invocation's own bit, a shuffle or broadcast returns the invocation's value, an elect is true and a subgroup reduction is the value itself, so no `GroupNonUniform` capability is declared. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; 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 ### 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. + +- 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. - [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. @@ -24,6 +32,7 @@ - [3D image accesses bound to a 2D or 2D-array T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow CouchRiv's [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (gfx1036, #2023) and adriadam10's (gfx1035, #2140), not the PS5's gfx1013. On a 2D T#, `image_sample_lz` ignores r and reads the (u,v) texel, `image_load` and `image_load_mip` read the (x,y) texel when z is 0 and return zero otherwise, and `image_get_resinfo` returns (width, height, 1, levels). On a 2D-array T#, these accesses behave like dim 2D-array: r or z is the layer, and `image_get_resinfo` returns the layer count as its third component. Every other access of such an image, including samples with an LOD, bias, derivatives, offset or comparison, gathers, `image_get_lod` and stores, keeps throwing because it is not measured. - [Unnormalized S#s](../../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - [Unnormalized texel offsets](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) are added to the f32 coordinates: exact on RDNA2 (gfx1035), which applies the offset before the clamp ([measured by adriadam10](https://github.com/boykopovar/AnyPS5/pull/822#issuecomment-6033192725)). +- [Cube image gradients](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (shader recompiler) - `image_sample_d` and `image_sample_cd` on a cube pass their gradients to the host sample without the -1.0 that moves the face coordinates from [1, 2] to [0, 1], as Mesa's AMD lowering builds them (`src/amd/common/nir/ac_nir_lower_tex_coords.c`, `prepare_cube_coords`: face-space derivatives with no offset, coordinates offset by 1.5). Measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md): on a 16x16 32_FLOAT cube with five levels, point mip filter, `image_sample_d` reads the level of `round(log2(16 * max(|ddx|, |ddy|)))` for gradients of 2^(level ± 0.3) texels on each of the four components, both signs, mixed components, every face and two coordinates (1032 rows). `image_sample_cd` takes one level per quad of lanes, from the gradients of its first lane; the host sample computes it per lane. - [Image atomics](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. - [`s_memtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on RADV with a GFX10.3 to GFX11.7 GPU (RDNA 2, RDNA 3, RDNA 3.5) reads the Vulkan device clock instead of the subgroup clock, which [ACO](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/compiler/instruction_selection/aco_select_nir_intrinsics.cpp) reads from the 20-bit `SHADER_CYCLES` register there and which wraps every 2^20 cycles. The device clock is a monotonic 64-bit counter, but it counts the 100 MHz reference clock (the same one as `s_memrealtime`), not shader cycles, so `s_memtime` deltas are about 20 times smaller than on hardware. The families are matched by the name RADV reports ([`NarrowSubgroupClock`](../../../core/libs/prx/libSceAgcDriver/Execution/src/SubgroupClock.cpp)); a family added to Mesa later reads the subgroup clock until it is listed. AMD's proprietary Windows driver also exposes a 20-bit subgroup clock on device 0x1114 (Radeon 860M, measured in #1615), device 0x744c (Radeon RX 7900 XT, measured in #1632), and device 0x747e (Radeon RX 7800 XT, measured in #1632); other AMD proprietary devices remain unverified. - [`s_memtime` and `s_memrealtime`](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) on llvmpipe read only the low 32 bits of the host's nanosecond clock at either scope: Mesa's [`emit_clock`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gallium/auxiliary/gallivm/lp_bld_nir_soa.c) builds the high dword with a left shift by 32, so it is always 0 and both counters wrap every 2^32 ns (about 4.3 s). The [shader clock test](../../../core/libs/prx/libSceAgcDriver/tests/execution/ShaderClock.cpp) compares them modulo 2^32 there @@ -36,7 +45,7 @@ - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) reads the initial rounding fields from `GuestContext::floatMode`; missing metadata retains the legacy round-to-nearest-even assumption. Immediate rounding writes are accepted only when they preserve that state. State-changing or SGPR-sourced MODE writes throw until runtime mode tracking and affected arithmetic are implemented. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. -- A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. +- A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - 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. @@ -73,13 +82,17 @@ - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. +- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. - [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. - [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. - 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). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. -- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255; that measurement also returns 0 for 8-bit UNORM, which this path still compares. A bilinear cube of those 8-bit sRGB formats without `DISABLE_CUBE_WRAP` returns 0 for the same reason; float, unorm and snorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (except on the 8-bit formats below) and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. A bilinear cube of those 8-bit formats without `DISABLE_CUBE_WRAP`, and an anisotropic S# on them, return 0 for the same reason; float, snorm and the other unorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [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. @@ -97,5 +110,7 @@ - [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. - [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. Whole-word uses still throw. 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. +- [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. diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md deleted file mode 100644 index ee2e485fdb..0000000000 --- a/docs/user/COMPATIBILITY.md +++ /dev/null @@ -1,18 +0,0 @@ -# Tested compatibility - -| Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 3.4GHz | Intel(R) HD Graphic 620 / i5-7200 2.5GHz | -|----------------|-----------|-------------------|-------|------------------------------|------------------------------------------| -| Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | 36 FPS | -| DOOM Shareware (homebrew) | PPSA99666 | In game (E1M1 and later maps) | In game (E1M1) | ? | ? | - -DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit/ps5-doom), release v1.1.1 (content version 01.001.001). - -- Windows 11, RTX 4070 (version 01.001.000, an integration build before [#553](https://github.com/boykopovar/AnyPS5/pull/553), [#683](https://github.com/boykopovar/AnyPS5/pull/683), [#684](https://github.com/boykopovar/AnyPS5/pull/684) and [#685](https://github.com/boykopovar/AnyPS5/pull/685) were merged): movement and shooting in E1M1. [In-game screenshots](https://github.com/boykopovar/AnyPS5/pull/685#issuecomment-5997547697). -- Windows 11, RTX 4070 SUPER (v1.1.1, `main` `2edaf7f4` plus open PRs, reported in [#690](https://github.com/boykopovar/AnyPS5/pull/690)): further maps, weapon switching, new games, level select, save/load round trip and audible audio. -- Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. -- Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. -- Linux, CachyOS, AMD Ryzen 5 PRO 5650U integrated graphics (RADV RENOIR), Mesa 26.2.4, kernel 7.2.9, KDE Plasma 6.7.5 Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 and E1M2 completed at 35 FPS in game with a DualShock 4 through SDL (both sticks, D-pad, touchpad, options menu), audio heard, save/load round trip in `/download0`. The first run without a shader cache stayed on AGC compute presentation. `/dev/udmabuf` was accessible to the user, so shared direct memory was imported as dma-buf. -- Windows 11 Pro 10.0.26300, AMD Radeon RX 9060 XT (RDNA 4, driver 32.0.31041.1004) and the Ryzen 9 7950X integrated graphics (RDNA 2, driver 32.0.21045.5002), selected with `ANYPS5_GPU` (v1.1.1, `main` `b42ef24e0` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from their SELF and fSELF containers before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): on both devices, menus, a new game, movement, item pickups and firing in E1M1 with an Xbox One controller through SDL, audio heard. The first run without a shader cache was choppy and later runs were smooth; the frame rate was not measured. Later maps and save/load not tested. The `/download0` directory was created before the first run. -- On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). - -Not verified: Linux with Intel GPUs. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 83876a67d3..950c7b0b4e 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -12,6 +12,8 @@ Supported input sources are: - `MOUSE:Left`, `MOUSE:Middle`, `MOUSE:Right`, `MOUSE:X1`, or `MOUSE:X2`. - `WHEEL:Up` or `WHEEL:Down` for pad buttons. +While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS5 hides and captures the host cursor, so the title's own cursor is the only one; the capture ends when the window loses focus or when the mouse-to-stick toggle (`ToggleMouse`) is on. Mouse movement reaches the title scaled from the window's presented image to the title's display resolution, so the title's cursor moves as far as the host cursor would. + Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index ea2ef030a3..4e75ad5559 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -1,8 +1,10 @@ # Relinker usage +[**Online relinker**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. + ## Input and conversion -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. A bundled module in a SELF container is an error, as the executable is. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. ```text source/ @@ -43,10 +45,13 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | Option | Effect | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--help` | Print usage to stdout and exit with code `0`; input and output paths are not required. | | `--windows` | Produce a Windows PE executable. | | `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. | | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | +| `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--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. | @@ -57,13 +62,14 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | | `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | +| `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | | `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | -Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. +Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`. The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, and `--lazy-binding` flags are deprecated. If the application runs with these flags enabled, it will be extremely unstable and unsuitable for general use. These flags are only for debugging. @@ -71,6 +77,8 @@ 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. + Paths are relative to the output executable: ```text @@ -106,6 +114,8 @@ Windows PowerShell: .\app.exe ``` +A Windows executable started without a standard output or error handle, such as a `--windows-gui` build started from Explorer, writes that output to `anyps5.log` beside it, replacing the previous run's file. Without write access to that directory it runs without the log. + ### Controller input for test runs Set `APS5_PAD_INPUT_FILE` to an absolute path before launching the application to enable controller pulses from a local command file. Once the application is reading controller input, send a button with the helper from the repository root: @@ -148,9 +158,22 @@ Do not disable Dozen if it is the only Vulkan driver available; the application GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. +### Tracing system library calls + +Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. + +``` +last traced system library calls (APS5_TRACE_CALLS): + #1 thread 9384 libkernel.prx k+AXqu2-eBc from 0x0000000140010006 app.exe+0x10006 +most called traced imports (1 calls in total): + 1 libkernel.prx k+AXqu2-eBc +``` + +Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. + ## Exit codes -`0`: conversion succeeded. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. +`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. ## Import audit @@ -168,7 +191,9 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `implemented` | A built library exports the NID and its function is not a throwing stub. | | `stub` | Exported, but the whole function calls `NotImplemented_nid_no_patch`: the game loads and throws when it calls it. | | `absent` | No built library exports the NID: the loader fails. | -| `module` | The import names a file found in `--modules` (the title's own library); its exports are not checked. | +| `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | + +Each `--modules` file is also audited itself: its undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries it needs must have a file in `--libs` or `--modules`. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. The report also lists needed libraries that have no file in `--libs` or `--modules`, and imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. diff --git a/tools/build_web.py b/tools/build_web.py new file mode 100644 index 0000000000..849042afd4 --- /dev/null +++ b/tools/build_web.py @@ -0,0 +1,97 @@ +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import urllib.request +import zipfile + +ROOT = Path(__file__).resolve().parent.parent + + +def copy_sources(output): + source = ROOT / "demo/web" + for file in source.iterdir(): + if file.is_file(): + shutil.copy2(file, output / file.name) + (output / ".nojekyll").touch() + + +def request(url): + headers = {"User-Agent": "AnyPS5-Pages"} + token = os.environ.get("GITHUB_TOKEN") + if token and url.startswith("https://api.github.com/"): + headers["Authorization"] = f"Bearer {token}" + return urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=60) + + +def publish_libraries(output): + with request("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest") as response: + release = json.load(response) + tag = release["tag_name"] + if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", tag): + raise ValueError("Invalid release tag") + manifest = {"tag": tag, "url": release["html_url"], "platforms": {}} + destination = output / "libraries" + destination.mkdir(exist_ok=True) + for platform in ("windows", "linux"): + name = f"prx-{platform}-{tag}.zip" + matches = [asset for asset in release["assets"] if asset["name"] == name] + if len(matches) != 1: + raise ValueError(f"Missing or ambiguous release asset: {name}") + asset = matches[0] + temporary = destination / f"{platform}.download" + try: + digest = hashlib.sha256() + size = 0 + with request(asset["browser_download_url"]) as response, temporary.open("wb") as target: + while chunk := response.read(1024 * 1024): + size += len(chunk) + if size > asset["size"] or size > 512 * 1024 * 1024: + raise ValueError("Release archive exceeds expected size") + digest.update(chunk) + target.write(chunk) + if size != asset["size"]: + raise ValueError("Incomplete release archive") + checksum = digest.hexdigest() + if asset.get("digest") and asset["digest"] != "sha256:" + checksum: + raise ValueError("Release archive checksum mismatch") + with zipfile.ZipFile(temporary) as archive: + files = archive.infolist() + if not files or len(files) > 4096: + raise ValueError("Invalid library archive") + for file in files: + if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: + raise ValueError("Invalid library archive layout") + corrupt = archive.testzip() + if corrupt: + raise ValueError(f"Corrupt release entry: {corrupt}") + expanded = sum(file.file_size for file in files) + path = f"libraries/{platform}-{checksum}.zip" + temporary.replace(output / path) + manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum} + finally: + temporary.unlink(missing_ok=True) + (output / "release.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(f"Published library assets from {tag}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--build", type=Path, required=True) + parser.add_argument("--libraries", action="store_true") + args = parser.parse_args() + output = args.build.resolve() / "web" + for name in ("relinker_wasm.mjs", "relinker_wasm.wasm"): + if not (output / "native" / name).is_file(): + raise ValueError(f"Build relinker_wasm first: missing {name}") + copy_sources(output) + if args.libraries: + publish_libraries(output) + print(output) + + +if __name__ == "__main__": + main() diff --git a/tools/check_conventions.py b/tools/check_conventions.py index de46dd4d96..34b639f166 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -29,7 +29,7 @@ RULES = { CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} -AGENT_FILES = {"AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursorrules", "copilot-instructions.md"} +AGENT_FILES = {"agents.md", "claude.md", "gemini.md", ".cursorrules", "copilot-instructions.md"} DEBT = "docs/dev/TechnicalDebt.md" DEBT_DIR = "docs/dev/debt/" DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") @@ -109,7 +109,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"): + if match 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: @@ -324,7 +324,7 @@ def library(path): def allowed_notes(path): p = PurePosixPath(path) - if p.name in AGENT_FILES: + if p.name.lower() in AGENT_FILES: return False if p.suffix.lower() not in NOTES: return None @@ -397,5 +397,5 @@ if __name__ == "__main__": if pr: check.pull_request(pr["title"], pr["body"]) elif args.title is not None or args.body: - check.pull_request(args.title or "", open(args.body, encoding="utf-8").read() if args.body else "") + check.pull_request(args.title or "", open(args.body, encoding="utf-8-sig").read() if args.body else "") raise SystemExit(output(check, os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5"))) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 0fb47521a6..7b00d94d3e 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -11,7 +11,17 @@ KNOWN_UNRUN = set() def built(build): - listed = subprocess.run(["ninja", "-C", str(build), "-t", "targets", "all"], + program, generator = None, None + for line in (Path(build) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + if line.startswith("CMAKE_MAKE_PROGRAM:"): + program = line.partition("=")[2] + elif line.startswith("CMAKE_GENERATOR:"): + generator = line.partition("=")[2] + if generator not in ("Ninja", "Ninja Multi-Config"): + raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") + if not program or program.endswith("-NOTFOUND"): + raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + listed = subprocess.run([program, "-C", str(build), "-t", "targets", "all"], capture_output=True, text=True, check=True).stdout names = set() for line in listed.splitlines(): diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 46b59c202c..df15c544a7 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -1,6 +1,7 @@ import argparse import hashlib import os +import operator import shlex import shutil import struct @@ -63,8 +64,12 @@ def oracle(): @cache +def detected_target(binary): + return subprocess.run([binary, "--target"], capture_output=True, text=True, check=True).stdout.strip() + + def target(): - return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle()) def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): @@ -89,7 +94,16 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): - rows = [tuple(r) for r in rows] + validated = [] + for index, row in enumerate(rows, 1): + try: + values = tuple(operator.index(value) for value in row) + except TypeError as error: + raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") from error + if len(values) != 4 or any(value < 0 or value > 0xffffffff for value in values): + raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") + validated.append(values) + rows = validated wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) @@ -111,6 +125,13 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de return out[:len(rows)] +def parse_word(value): + try: + return int(value, 0) + except ValueError: + return int(value, 10) + + def main(): parser = argparse.ArgumentParser(description="Run an RDNA kernel body on the local AMD GPU, one input row per lane.") parser.add_argument("body", type=Path, help="assembly inserted into template.s") @@ -128,7 +149,7 @@ def main(): parser.add_argument("--extra", type=Path, help="bytes appended after the rows") parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") args = parser.parse_args() - rows = [tuple(int(v, 0) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] + rows = [tuple(parse_word(v) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] if any(len(r) != 4 for r in rows): sys.exit("every row needs 4 values") extra = args.extra.read_bytes() if args.extra else b"" diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 7de671b40d..bdf6940a72 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -76,6 +76,45 @@ class OracleCacheTests(unittest.TestCase): self.assertEqual([file for file in cache.rglob("*") if file.is_file()], [binary]) +class InputRowsTests(unittest.TestCase): + def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): + for row in ((1, 2, 3), (1, 2, 3, 4, 5), (-1, 0, 0, 0), + (0x100000000, 0, 0, 0), (1.5, 0, 0, 0), ("1", 0, 0, 0)): + with self.subTest(row=row), patch.object(hw_oracle, "assemble", side_effect=AssertionError("assembler called")) as assemble: + with self.assertRaisesRegex(ValueError, "row 1"): + hw_oracle.run("s_nop 0", [row], **MODES) + assemble.assert_not_called() + + def test_unsigned_bits_and_indexable_values_are_preserved(self): + class Indexable: + def __index__(self): + return 0xffffffff + + def dispatch(command, **kwargs): + data = Path(command[3]).read_bytes() + self.assertEqual(data[:16], hw_oracle.struct.pack("<4I", 0, 0xffffffff, 0x80000000, 1)) + self.assertEqual(data[16:32 * 16], bytes(31 * 16)) + self.assertEqual(data[32 * 16:], b"extra") + Path(command[4]).write_bytes(bytes(32 * 64)) + + with patch.object(hw_oracle, "assemble", return_value="kernel.co"), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + results = hw_oracle.run("s_nop 0", [(0, Indexable(), 0x80000000, True)], b"extra", **MODES) + self.assertEqual(results, [tuple([0] * 16)]) + + def test_cli_accepts_zero_padded_decimal_rows(self): + with tempfile.TemporaryDirectory() as tmp: + body, rows = Path(tmp) / "body.s", Path(tmp) / "rows.txt" + body.write_text("s_nop 0") + rows.write_text("0001 08 0xFFFFFFFF 0b10\n") + argv = ["hw_oracle.py", str(body), str(rows)] + argv += [item for name, value in MODES.items() for item in ["--" + name.replace("_", "-"), str(value)]] + with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run: + hw_oracle.main() + self.assertEqual(run.call_args.args[1], [(1, 8, 0xffffffff, 2)]) + + class FloatModeTests(unittest.TestCase): def test_python_requires_each_mode(self): for name in MODES: @@ -127,6 +166,41 @@ class FloatModeTests(unittest.TestCase): self.assertEqual(assemble.call_args.kwargs, MODES | {"lds": 4096}) +class TargetOverrideTests(unittest.TestCase): + def setUp(self): + detector = getattr(hw_oracle, "detected_target", hw_oracle.target) + detector.cache_clear() + + def tearDown(self): + detector = getattr(hw_oracle, "detected_target", hw_oracle.target) + detector.cache_clear() + + def test_override_is_read_for_each_call_without_a_gpu_probe(self): + with patch.object(hw_oracle, "oracle", side_effect=AssertionError("GPU probe")): + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1030"}): + self.assertEqual(hw_oracle.target(), "gfx1030") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + + def test_removing_override_restores_cached_detection_for_that_binary(self): + reply = subprocess.CompletedProcess([], 0, "gfx1030\n", "") + with patch.object(hw_oracle, "oracle", return_value=Path("oracle-a")), \ + patch.object(hw_oracle.subprocess, "run", return_value=reply) as probe: + with patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + self.assertEqual(hw_oracle.target(), "gfx1030") + self.assertEqual(hw_oracle.target(), "gfx1030") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): + self.assertEqual(hw_oracle.target(), "gfx1100") + with patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + self.assertEqual(hw_oracle.target(), "gfx1030") + self.assertEqual(probe.call_count, 1) + with patch.object(hw_oracle, "oracle", return_value=Path("oracle-b")), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + probe.return_value = subprocess.CompletedProcess([], 0, "gfx1200\n", "") + self.assertEqual(hw_oracle.target(), "gfx1200") + self.assertEqual(probe.call_count, 2) + + class OracleConfigurationTests(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") def test_cache_and_compiler_command_with_host_toolchain(self): diff --git a/tools/import_audit.py b/tools/import_audit.py index 276ce39f84..f0fd82f2d2 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -15,6 +15,7 @@ ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" NID = re.compile(r"[A-Za-z0-9+\-]{11}") NID_POSTFIX = "_nid_postfix" +MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -43,17 +44,112 @@ def cstring(data, offset, limit, path, what, encoding="utf-8", errors="surrogate raise AuditError(f"{path}: {what} at 0x{offset:x} is not {encoding}: {error}") from error -def elf_exports(path, data): +def elf_sections(path, data): header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") if not section_offset: - raise AuditError(f"{path}: ELF exports without a section header table are not supported") + raise AuditError(f"{path}: ELF file without a section header table is not supported") if not section_count: section_count = unpack("= virtual and address - virtual <= file_size and size <= file_size - (address - virtual): + result = offset + address - virtual + take(data, result, size, path, what) + return result + raise AuditError(f"{path}: {what} address 0x{address:x} is outside file-backed load segments") + + dynamic_segments = [segment for segment in segments if segment[0] == 2] + if len(dynamic_segments) != 1: + raise AuditError(f"{path}: expected one dynamic segment, found {len(dynamic_segments)}") + _, _, dynamic_offset, _, _, dynamic_size, _, _ = dynamic_segments[0] + if dynamic_size % 16: + raise AuditError(f"{path}: dynamic segment size {dynamic_size} is not a multiple of 16") + dynamic = {} + needed_offsets = [] + for position in range(0, dynamic_size, 16): + tag, value = unpack("> 4 in (1, 2, 10) and other & 3 in (0, 3): + exports.add(symbol) + needed = {cstring(data, string_offset + value, string_offset + string_size, path, "needed library name") for value in needed_offsets} + return imports, exports, needed + + +def elf_exports(path, data): + header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") + if not header[6]: + return elf_program_dynamic_symbols(path, data)[1] + sections = elf_sections(path, data) names = set() for section_index, section in enumerate(sections): kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] @@ -83,6 +179,38 @@ def elf_exports(path, data): return names +def elf_imports(path, data): + header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header") + if not header[6]: + imports, _, needed = elf_program_dynamic_symbols(path, data) + return imports, needed + sections = elf_sections(path, data) + symbols = set() + needed = set() + for section in sections: + kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] + if kind == 11: + 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})") + strings_offset, strings_size = sections[link][4], sections[link][5] + take(data, strings_offset, strings_size, path, "dynamic string table") + for position in range(0, size, 24): + name, _, _, index, _, _ = unpack("= len(sections) or sections[link][1] != 3: + raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})") + strings_offset, strings_size = sections[link][4], sections[link][5] + take(data, strings_offset, strings_size, path, "dynamic string table") + for position in range(0, size, 16): + tag, value = unpack("6} imports {summary['references_by_class'][kind]:>6} references") @@ -325,12 +488,18 @@ def render(summary, records, missing): if selected: lines.append("") lines.append(f"{title} ({len(selected)})") - current = None + current = False for record in selected: if record["library"] != current: current = record["library"] lines.append(f" [{current or 'library not named'}]") lines.append(f" {label(record)}") + if stops: + lines.append("") + lines.append("Current stop (first unresolved import, in registry order)") + for entry in stops: + stop = entry["stop"] + lines.append(f" {entry['registry']}: none" if stop is None else f" {entry['registry']}: {label(stop)} [{stop['library']}]") if missing: lines.append("") lines.append(f"Needed libraries with no file in --libs or --modules ({len(missing)})") @@ -345,7 +514,7 @@ def main(argv=None): parser = argparse.ArgumentParser(description="classify the imports of a converted game against the built system libraries") parser.add_argument("registries", nargs="+", type=Path, help="JSON written by 'relinker --registry'") parser.add_argument("--libs", action="append", required=True, help="directory of built .prx files; repeat for several") - parser.add_argument("--modules", action="append", default=[], help="directory of the title's own modules; libraries named there are not checked") + parser.add_argument("--modules", action="append", default=[], help="directory of the title's own modules; each module's own imports are audited") parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") @@ -359,16 +528,22 @@ def main(argv=None): output = args.json.resolve() if any(output == path.resolve() or (args.json.exists() and args.json.samefile(path)) for path in inputs): raise AuditError(f"{args.json}: JSON output would overwrite an input file") - imports = [entry for registry in args.registries for entry in read_registry(registry)] + registries = [(registry, read_registry(registry)) for registry in args.registries] + imports = [entry for _, entries in registries for entry in entries] exports, libraries = built_libraries(args.libs) - modules = module_files(args.modules) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules) + module_imports, module_exports = read_modules(args.modules) + records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports) if args.names: attach_names(records, load_names(args.names)) - summary = summarize(records, len(imports), missing) + summary = summarize(records, sum(record["references"] for record in records), missing) + stops = [{"registry": str(path), "stop": current_stop(entries, records)} for path, entries in registries] if args.json: commit, dirty = git_stamp(Path(args.source)) - result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "imports": records} + json_stops = [{"registry": entry["registry"], + "nid": entry["stop"]["nid"] if entry["stop"] else 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") except AuditError as error: print(f"FAIL: {error}", file=sys.stderr) @@ -376,7 +551,7 @@ def main(argv=None): except OSError as error: print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 - print(render(summary, records, missing)) + print(render(summary, records, missing, stops)) return 1 if summary["unique_by_class"]["absent"] or missing else 0 diff --git a/tools/package_release.py b/tools/package_release.py index 52f0b3ffc7..c825539e1d 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -9,6 +9,15 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent +def windows_runtime(build): + for line in (build / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + if line.startswith("CMAKE_CXX_COMPILER:"): + compiler = line.partition("=")[2] + if compiler and not compiler.endswith("-NOTFOUND"): + return Path(compiler).parent + raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {build / 'CMakeCache.txt'}") + + def package(platform, build, output, version): if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): raise ValueError(f"Invalid release tag for asset filenames: {version}") @@ -20,14 +29,19 @@ def package(platform, build, output, version): executable = "relinker.exe" if platform == "windows" else "relinker" files = [library for library in libraries if library.name in expected] if platform == "windows": - runtime = Path("C:/winlibs/mingw64/bin") + runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) binary = build / "core/relinker" / executable for file in [*files, binary]: if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") - output.mkdir(parents=True, exist_ok=True) asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" + names = (f"prx-{platform}-{version}.zip", f"prx-{platform}-{version}.tar.gz", asset) + for name in names: + destination = output / name + if destination.exists() and not destination.is_file(): + raise RuntimeError(f"Release destination is not a file: {destination}") + output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: staging = Path(temporary) with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 81ba717817..582f6bb071 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -20,6 +20,11 @@ def git(*args): return subprocess.run(["git", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True).stdout +def own(prs, sha): + return {n: pr for n, pr in prs.items() + if pr["sha"] != sha and subprocess.run(["git", "merge-base", "--is-ancestor", pr["sha"], sha], capture_output=True).returncode != 0} + + def gh(*args): return subprocess.run(["gh", *args], capture_output=True, text=True, encoding="utf-8", check=True).stdout @@ -75,7 +80,7 @@ def exports(diff): lines = diff.splitlines() for i, line in enumerate(lines): m = EXPORT.match(line) - if not m or m.group(1).endswith("_nid_no_patch"): + if not m or m.group(1).endswith(("_nid_no_patch", "_nid_no_patch_cut")): continue text = line[1:] for nxt in lines[i + 1:]: @@ -192,7 +197,7 @@ if __name__ == "__main__": fetch(args.base, prs) if args.branch: sha = git("rev-parse", args.branch).strip() - prs = {n: pr for n, pr in prs.items() if pr["sha"] != sha} + prs = own(prs, sha) prs[0] = {"ref": sha, "author": None, "depends": set()} for pr in prs.values(): scan(pr) diff --git a/tools/progress.py b/tools/progress.py index d2222e7206..cc18c35023 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -1,4 +1,5 @@ import argparse +import importlib.util import json import os import re @@ -6,6 +7,11 @@ import sys from html import escape from pathlib import Path +tokenizer_spec = importlib.util.spec_from_file_location("anyps5_cpp_tokenizer", Path(__file__).with_name("nid_names.py")) +tokenizer = importlib.util.module_from_spec(tokenizer_spec) +tokenizer_spec.loader.exec_module(tokenizer) +CPP_TOKEN = tokenizer.CPP_TOKEN + ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" OPCODES = ROOT / "core" / "shader" / "recompiler" / "RdnaDecoder" / "include" / "RdnaDecoder" / "RdnaOpcode.hpp" @@ -16,8 +22,10 @@ STUB = "NotImplemented_nid_no_patch" CALL = re.compile(r"\b(\w+)\s*\(") CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', re.M) SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) -STUB_WRAPPER = re.compile(r"\bstatic\s+(?:\[\[noreturn\]\]\s+)?[\w:<>,\s*&]+?\s+(\w+)\s*\([^;{]*\)\s*\{") -NON_CODE = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'|//[^\n]*|/\*.*?\*/', re.S) +STUB_WRAPPER = re.compile( + r"^[ \t]*(?:static\s+)?(?:\[\[noreturn\]\]\s+)?(?:(?!APS5_VABI)[\w:<>,\s*&])+?[\s*&]" + r"(?!(?:if|while|for|switch|catch|return|else)\b)(\w+)\s*\([^;{]*\)\s*" + r"(?:const\s*)?(?:noexcept\s*)?(?:->\s*[\w:<>*&\s]+?)?\{", re.M) DISCARD = re.compile(r"\(void\)\s*\w+\s*;") EMPTY_RETURN = re.compile(r"return\s*;") CONST_RETURN = re.compile(r"return\s+(?:[+-]?\s*(?:0[xX][0-9a-fA-F]+|\d+)[uUlL]*|\w+|\"(?:\\.|[^\"\\])*\")\s*;") @@ -70,14 +78,15 @@ DONE_COLOR, TODO_COLOR, BORDER, TEXT = "#2ea043", "#6e7681", "#0d1117", "#ffffff def blank(match): - if match.group(0).startswith(('"', "'")): + if match.group("code") is not None: + return match.group() + if match.group("comment") is None: return "LITERAL" - return re.sub(r"[^\n]", " ", match.group(0)) + return re.sub(r"[^\n]", " ", match.group()) def code(text): - return NON_CODE.sub(blank, text) - + return CPP_TOKEN.sub(blank, text.replace("\\\n", "").replace("\\\r\n", "")) def body_end(text, start): depth = 0 @@ -155,7 +164,7 @@ def scan_library(path): wrappers = inert_wrappers(text) for match in DEFINITION.finditer(text): name = match.group(1) - if name.endswith("_nid_no_patch"): + if name.endswith(("_nid_no_patch", "_nid_no_patch_cut")): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] if any(call.group(1) in calls for call in CALL.finditer(body)): @@ -169,7 +178,7 @@ def scan_library(path): "done_names": sorted(done), "todo_names": sorted(todo), "inert_names": sorted(inert)} if not (done or todo): cmake = path / "CMakeLists.txt" - texts = [(CMAKE_SHARED, cmake.read_text())] if cmake.is_file() else [] + texts = [(CMAKE_SHARED, cmake.read_text(encoding="utf-8", errors="ignore"))] if cmake.is_file() else [] texts += [(SOURCE_SHARED, source.read_text(errors="ignore")) for source in sorted(path.glob("*.cpp"))] owners = [m[1] for pattern, content in texts for m in pattern.finditer(content)] owners = [o for o in owners if o != path.name and (path.parent / o).is_dir()] @@ -194,12 +203,12 @@ def camel(name): def collect_shaders(): isa = {} - for line in ISA.read_text().splitlines(): + for line in ISA.read_text(encoding="utf-8", errors="ignore").splitlines(): if line and not line.startswith("#"): name, encoding = line.split() isa[name] = encoding by_camel = {camel(name): name for name in isa} - enum = re.search(r"enum class RdnaOpcode[^{]*\{(.*?)\};", OPCODES.read_text(), re.S).group(1) + enum = re.search(r"enum class RdnaOpcode[^{]*\{(.*?)\};", OPCODES.read_text(encoding="utf-8", errors="ignore"), re.S).group(1) opcodes = [o for o in re.findall(r"^\s*([A-Z]\w*)\s*[,=]", enum, re.M) if o not in OPCODE_SENTINELS] supported, extra = set(), [] for opcode in opcodes: @@ -363,6 +372,7 @@ def summary(libraries, shaders): f'

The total is the AMD RDNA 1 + RDNA 2 instruction list (tools/rdna_isa.txt). ' f'An instruction is implemented when the decoder recognizes it. ' f"Decoded opcodes not present in AMD's public list are not counted: {extra}.

", + '

Progress history (one point per day).

', "", ]) + "\n" @@ -439,6 +449,6 @@ if __name__ == "__main__": (output / "badge-libraries.svg").write_text(badge("libraries*", libraries)) (output / "badge-shaders.svg").write_text(badge("shaders", shaders)) (output / "progress.svg").write_text(render(libraries, shaders)) - (output / "index.html").write_text(summary(libraries, shaders)) + (output / "progress.html").write_text(summary(libraries, shaders)) print(f'libraries {libraries["done"]}/{libraries["total"]} ({libraries["percent"]}%)') print(f'shaders {shaders["done"]}/{shaders["total"]} ({shaders["percent"]}%)') diff --git a/tools/progress_history.py b/tools/progress_history.py new file mode 100644 index 0000000000..63159c40a5 --- /dev/null +++ b/tools/progress_history.py @@ -0,0 +1,139 @@ +import argparse +import io +import json +import subprocess +import tarfile +import tempfile +from datetime import date +from html import escape +from pathlib import Path + +import progress + +SERIES = (("libraries", "System libraries", "#2f81f7"), ("shaders", "GPU shader instructions", "#2ea043")) +SHORT = {"libraries": "Libraries", "shaders": "Shaders"} +WIDTH, HEIGHT, LEFT, RIGHT, TOP, BOTTOM = 900, 380, 50, 130, 20, 60 +MAX_DOTS = 45 +BACKGROUND, GRID, TEXT = "#0d1117", "#30363d", "#c9d1d9" + + +def git(*args): + return subprocess.run(["git", "-C", str(progress.ROOT), *args], check=True, capture_output=True).stdout + + +def daily_commits(ref): + seen = {} + for line in git("log", "--first-parent", "--format=%H %cs", ref).decode().splitlines(): + sha, day = line.split() + seen.setdefault(day, sha) + return sorted(seen.items()) + + +def measure(sha): + paths = [progress.PRX.relative_to(progress.ROOT).as_posix(), progress.OPCODES.relative_to(progress.ROOT).as_posix()] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + try: + tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") + except subprocess.CalledProcessError: + return None + saved = progress.PRX, progress.OPCODES + progress.PRX, progress.OPCODES = root / paths[0], root / paths[1] + try: + libraries, shaders = progress.collect_libraries(), progress.collect_shaders() + except (OSError, AttributeError): + return None + finally: + progress.PRX, progress.OPCODES = saved + return {name: {key: data[key] for key in ("done", "total", "percent")} + for name, data in (("libraries", libraries), ("shaders", shaders))} + + +def collect(ref): + points = [] + for day, sha in daily_commits(ref): + counts = measure(sha) + if counts: + points.append({"date": day, "commit": sha, **counts}) + return points + + +def labelled(day, span): + if span <= 31: + return day.strftime("%m-%d") + if span <= 150: + return day.strftime("%m-%d") if day.weekday() == 0 else "" + return day.strftime("%Y-%m") if day.day <= 7 and day.weekday() == 0 else "" + + +def chart(points): + days = [date.fromisoformat(p["date"]).toordinal() for p in points] + first, span = days[0], max(days[-1] - days[0], 1) + top = max(10, int(-(-max(p[name]["percent"] for p in points for name, _, _ in SERIES) // 10) * 10)) + px = lambda d: LEFT + (d - first) / span * (WIDTH - LEFT - RIGHT) + py = lambda v: TOP + (1 - v / top) * (HEIGHT - TOP - BOTTOM) + base = py(0) + out = [f'', + f''] + for tick in range(0, top + 1, 10): + out.append(f'' + f'{tick}%') + for d, p in zip(days, points): + x = px(d) + label = labelled(date.fromordinal(d), span) + if label: + out.append(f'' + f'{label}') + for name, label, color in SERIES: + coords = [(px(d), py(p[name]["percent"])) for d, p in zip(days, points)] + line = " ".join(f"{x:.1f},{y:.1f}" for x, y in coords) + out.append(f'' + f'') + for (x, y), p in zip(coords, points) if len(points) <= MAX_DOTS else (): + out.append(f'{p["date"]}: {label} ' + f'{p[name]["percent"]}% ({p[name]["done"]}/{p[name]["total"]})') + last = points[-1][name] + out.append(f'' + f'{SHORT[name]} {last["percent"]}%') + out.append("") + return "\n".join(out) + + +def page(points): + rows = "\n".join(f'{p["date"]}' + "".join( + f'{p[name]["done"]}/{p[name]["total"]}{p[name]["percent"]}%' for name, _, _ in SERIES) + "" + for p in reversed(points)) + return "\n".join([ + '', + '', + 'AnyPS5 progress history', + '', + '', + '

Progress history

', + chart(points), + f'

One point per day with commits on main, taken at the last commit of the day. Every point is measured ' + f'with the current tools/progress.py rules and instruction list, ' + 'so the series is comparable across days. Hover a point for the exact numbers. Raw numbers: history.json. ' + 'Back to the current progress.

', + '' + "".join(f'' for _, label, _ in SERIES) + '', + rows, + '
Date{escape(label)}%
', + '', + ]) + "\n" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("output", type=Path) + parser.add_argument("--ref", default="HEAD", help="branch or commit whose first-parent history is measured") + args = parser.parse_args() + points = collect(args.ref) + if not points: + parser.error("no commit with a measurable tree") + args.output.mkdir(parents=True, exist_ok=True) + (args.output / "history.json").write_text(json.dumps(points, indent=2)) + (args.output / "history.svg").write_text(chart(points) + "\n") + (args.output / "history.html").write_text(page(points)) + print(f"{len(points)} days, {points[0]['date']} .. {points[-1]['date']}") diff --git a/tools/stubgen.py b/tools/stubgen.py index 492f485b53..2c296b9298 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -211,6 +211,8 @@ def main(argv=None): ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, which go in docs/dev/debt/MODULE.md, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", newline="\n") try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") diff --git a/tools/test_web.py b/tools/test_web.py new file mode 100644 index 0000000000..c6c66fe237 --- /dev/null +++ b/tools/test_web.py @@ -0,0 +1,34 @@ +import argparse +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import urllib.request + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "core/relinker/relinker/tests")) +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--build", type=Path, required=True) + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="anyps5-web-") as directory: + fixture = Path(directory) + (fixture / "main.elf").write_bytes(main_fixture()) + (fixture / "module.elf").write_bytes(module_with_symbol(True)) + source = (args.build / "web/archive.mjs").read_text(encoding="utf-8") + url = re.search(r'from "(https://cdn.jsdelivr.net/[^"\n]+)"', source).group(1) + library = fixture / "zip.mjs" + with urllib.request.urlopen(url, timeout=30) as response: + library.write_bytes(response.read()) + environment = dict(os.environ, ANYPS5_TEST_ZIP_URL=url, ANYPS5_TEST_ZIP_PATH=library.as_uri()) + subprocess.run(["node", str(ROOT / "demo/web/tests/archive.test.mjs"), str(args.build.resolve() / "web"), str(fixture)], env=environment, check=True) + + +if __name__ == "__main__": + main() diff --git a/tools/tests/test_check_conventions_agent_files.py b/tools/tests/test_check_conventions_agent_files.py new file mode 100644 index 0000000000..48a975aa1b --- /dev/null +++ b/tools/tests/test_check_conventions_agent_files.py @@ -0,0 +1,24 @@ +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from check_conventions import allowed_notes + + +class AgentFileTests(unittest.TestCase): + def test_agent_files_are_rejected_in_any_case(self): + for name in ("CLAUDE.md", "claude.md", "Claude.md", "AGENTS.md", "Agents.md", "GEMINI.md", "Copilot-Instructions.md", ".CursorRules"): + with self.subTest(name=name): + self.assertIs(allowed_notes(f"docs/dev/{name}"), False) + self.assertIs(allowed_notes(name), False) + + def test_ordinary_docs_are_still_allowed(self): + self.assertTrue(allowed_notes("docs/dev/Claudette.md")) + self.assertTrue(allowed_notes("README.md")) + self.assertIs(allowed_notes("core/x.cpp"), None) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_body_bom.py b/tools/tests/test_check_conventions_body_bom.py new file mode 100644 index 0000000000..2bc30ad43f --- /dev/null +++ b/tools/tests/test_check_conventions_body_bom.py @@ -0,0 +1,51 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +BODY = ( + "### What\nFix x.\n\n### Tested\nNot tested.\n\n### Checklist\n" + "- [x] Depends on: none\n- [x] AI-assisted: no\n" +) + + +class BodyBomTests(unittest.TestCase): + def run_checker(self, body): + 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", "user.name=Conventions Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + (root / "README.md").write_text("# Example\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: add readme") + (root / "body.md").write_bytes(body) + return subprocess.run( + [sys.executable, "-I", str(checker), "--base", "HEAD", "--head", "HEAD", + "--title", "fix: x", "--body", "body.md"], + cwd=root, capture_output=True, text=True, encoding="utf-8", + env={key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, + ) + + def test_body_with_utf8_bom_keeps_its_first_section(self): + result = self.run_checker(b"\xef\xbb\xbf" + BODY.encode("utf-8")) + self.assertNotIn("section 'What' is missing", result.stdout) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_body_without_bom_passes(self): + result = self.run_checker(BODY.encode("utf-8")) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py index 57df9623ed..258fc2916a 100644 --- a/tools/tests/test_check_conventions_wrapped_declarations.py +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -10,7 +10,7 @@ DEFINITION = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" class WrappedDeclarationTests(unittest.TestCase): - def check(self, first_file, first_text): + def check(self, first_file, first_text, second_text=DEFINITION): with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -31,7 +31,7 @@ class WrappedDeclarationTests(unittest.TestCase): git("commit", "-qm", "feat: add library a") second = root / "core/libs/prx/libSceB/Export.cpp" second.parent.mkdir(parents=True) - second.write_text(DEFINITION, encoding="utf-8") + second.write_text(second_text, encoding="utf-8") git("add", ".") git("commit", "-qm", "feat: add library b") env = {key: value for key, value in os.environ.items() @@ -47,6 +47,12 @@ class WrappedDeclarationTests(unittest.TestCase): self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_cut_helpers_in_different_libraries_are_not_duplicate_nids(self): + helper = "int APS5_VABI Support_nid_no_patch_cut() { return 3; }\n" + result = self.check("Export.cpp", helper, helper) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + def test_wrapped_definition_is_a_duplicate(self): result = self.check("Export.cpp", "int APS5_VABI sceFooBar(int a,\n int b) {\n return 0;\n}\n") self.assertEqual(result.returncode, 1, result.stdout + result.stderr) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 5534297e02..1e84d84ea6 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1,6 +1,7 @@ import contextlib import io import json +import os import struct import sys import tempfile @@ -20,23 +21,80 @@ MESSAGE_NID = "wTpfglkmv34" MESSAGE_NAME = "sceMsgDialogProgressBarSetValue" -def build_elf(symbols): +def build_elf(symbols, needed=()): strings = b"\0" entries = [struct.pack("⚪ 1 behaviourally empty (constant return or throw)", report) + def test_cut_helpers_are_not_system_exports(self): + with tempfile.TemporaryDirectory() as directory: + library = Path(directory) / "libSceExample" + library.mkdir() + (library / "Export.cpp").write_text( + "int APS5_VABI Ready_nid_no_patch_cut() { return 1; }\n" + "int APS5_VABI Pending_nid_no_patch_cut() { NotImplemented_nid_no_patch(__func__); }\n" + "int APS5_VABI Real() { return 2; }\n", encoding="utf-8") + group = progress.scan_library(library) + self.assertEqual(group["done_names"], ["Real"]) + self.assertEqual(group["todo_names"], []) + self.assertEqual(group["inert_names"], ["Real"]) + def test_empty_libraries(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) diff --git a/tools/tests/test_progress_history.py b/tools/tests/test_progress_history.py new file mode 100644 index 0000000000..c8da6a22c0 --- /dev/null +++ b/tools/tests/test_progress_history.py @@ -0,0 +1,59 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from datetime import date +from pathlib import Path +from unittest.mock import patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import progress +import progress_history + +OPCODES = Path("core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp") + + +def commit(root, day, message, body): + library = root / "core/libs/prx/libSceExample" + library.mkdir(parents=True, exist_ok=True) + (library / "Export.cpp").write_text(body) + (root / OPCODES).parent.mkdir(parents=True, exist_ok=True) + (root / OPCODES).write_text("enum class RdnaOpcode : int {\n SEndpgm,\n};\n") + env = {**os.environ, "GIT_AUTHOR_DATE": f"{day}T12:00:00Z", "GIT_COMMITTER_DATE": f"{day}T12:00:00Z"} + subprocess.run(["git", "-C", str(root), "add", "-A"], check=True) + subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", message], + check=True, env=env) + + +class ProgressHistoryTests(unittest.TestCase): + def test_one_point_per_day_at_the_last_commit(self): + stub = "static void Stub() { NotImplemented_nid_no_patch(__func__); }\n" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + subprocess.run(["git", "-C", directory, "init", "-q"], check=True) + commit(root, "2026-01-01", "first", "int APS5_VABI A() { Stub(); return 0; }\n" + stub) + commit(root, "2026-01-02", "second", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { return 0; }\n") + commit(root, "2026-01-02", "third", "int APS5_VABI A() { return 0; }\nint APS5_VABI B() { Stub(); return 0; }\n" + stub) + with patch.multiple(progress, ROOT=root, PRX=root / "core/libs/prx", OPCODES=root / OPCODES): + points = progress_history.collect("HEAD") + html = progress_history.page(points) + self.assertEqual([p["date"] for p in points], ["2026-01-01", "2026-01-02"]) + self.assertEqual([(p["libraries"]["done"], p["libraries"]["total"]) for p in points], [(0, 1), (1, 2)]) + self.assertIn(" int " + STUB_BODY + CALLER) + + def test_pointer_next_to_name_helper(self): + self.assertTodo("static int *Helper() { NotImplemented_nid_no_patch(__func__); return nullptr; }\n" + CALLER) + + def test_static_helper_still_works(self): + self.assertTodo("static int Helper() " + STUB_BODY + CALLER) + + def test_unrelated_helper_stays_done(self): + group = scan("namespace {\nint Other() { return 1; }\nint Helper() " + STUB_BODY + "}\n" + "int APS5_VABI sceExampleCall() { return Other(); }\n") + self.assertEqual(group["done_names"], ["sceExampleCall"]) + self.assertEqual(group["todo_names"], []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index e0d0cde7c7..66ab9bd1b3 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -72,7 +72,7 @@ class StubGeneratorTests(unittest.TestCase): nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") original = "AAAAAAAAAAA sceExample\n" database.write_text(original, encoding="utf-8") - output.hardlink_to(database) + os.link(database, output) arguments = ["--module", "libSceExample", "--nids", str(nids), "--db", str(database), "--out", str(output)] with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: stubgen.main(arguments) diff --git a/tools/tests/test_stubgen_stdout_newlines.py b/tools/tests/test_stubgen_stdout_newlines.py new file mode 100644 index 0000000000..93b49efe1c --- /dev/null +++ b/tools/tests/test_stubgen_stdout_newlines.py @@ -0,0 +1,33 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +tools = Path(__file__).resolve().parents[1] + + +class StubgenStdoutNewlineTests(unittest.TestCase): + def run_stubgen(self, *extra): + with tempfile.TemporaryDirectory() as d: + nids = Path(d) / "nids.txt" + nids.write_text("AC1FtjqMCL0\n", encoding="utf-8", newline="\n") + return subprocess.run( + [sys.executable, str(tools / "stubgen.py"), "--module", "libkernel", "--nids", str(nids)] + list(extra), + capture_output=True, + check=True, + ) + + def test_code_on_stdout_uses_lf(self): + result = self.run_stubgen() + self.assertIn(b"\n", result.stdout) + self.assertNotIn(b"\r\n", result.stdout) + + def test_td_only_on_stdout_uses_lf(self): + result = self.run_stubgen("--td-only") + self.assertIn(b"\n", result.stdout) + self.assertNotIn(b"\r\n", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index 3d91bf0d65..e9862c2bc3 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -1,4 +1,6 @@ from pathlib import Path +import os +import shutil import sys import tempfile import unittest @@ -36,13 +38,21 @@ class RegistrationTests(unittest.TestCase): def test_hyphenated_executable_is_discovered(self): output = "tests/buffer-bounds_tests: CXX_EXECUTABLE_LINKER\n" - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)): - self.assertEqual(check_test_registration.built("build"), {"buffer-bounds_tests"}) + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + self.assertEqual(check_test_registration.built(build), {"buffer-bounds_tests"}) + self.assertEqual(run.call_args[0][0][0], "/configured/ninja") def test_nested_test_output_is_discovered(self): output = "tests/gpu/buffer_bounds_tests: CXX_EXECUTABLE_LINKER\n" - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)): - self.assertEqual(check_test_registration.built("build"), {"buffer_bounds_tests"}) + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja Multi-Config\n", encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + self.assertEqual(check_test_registration.built(build), {"buffer_bounds_tests"}) + self.assertEqual(run.call_args[0][0][0], "/configured/ninja") def test_quoted_cmake_path_is_unescaped(self): names = self.registered(r'add_test("bounds" "/build/tests/a\"b_tests")' + '\n') @@ -118,7 +128,28 @@ class RegistrationTests(unittest.TestCase): (external / "CTestTestfile.cmake").write_text( 'add_test("bounds" "/build/tests/buffer_bounds_tests")\n', encoding="utf-8") self.assertEqual(check_test_registration.registered(build), {"buffer_bounds_tests"}) + def test_unsupported_generator_never_runs_build_tool(self): + for generator in ("Unix Makefiles", "Visual Studio 17 2022", ""): + with self.subTest(generator=generator), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=make\nCMAKE_GENERATOR:INTERNAL=" + generator + "\n", encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run") as run: + with self.assertRaisesRegex(ValueError, "requires a Ninja generator"): + check_test_registration.built(build) + run.assert_not_called() + def test_missing_make_program_never_runs_build_tool(self): + for program in (None, "", "CMAKE_MAKE_PROGRAM-NOTFOUND"): + with self.subTest(program=program), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + cache = "CMAKE_GENERATOR:INTERNAL=Ninja\n" + if program is not None: + cache += "CMAKE_MAKE_PROGRAM:FILEPATH=" + program + "\n" + (build / "CMakeCache.txt").write_text(cache, encoding="utf-8") + with patch.object(check_test_registration.subprocess, "run") as run: + with self.assertRaises(FileNotFoundError): + check_test_registration.built(build) + run.assert_not_called() if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py new file mode 100644 index 0000000000..fd02c62e37 --- /dev/null +++ b/tools/tests/test_trace_digest.py @@ -0,0 +1,129 @@ +import contextlib +import gzip +import io +import json +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import trace_digest + + +def complete(tid, name, ts, dur): + return {"name": name, "ph": "X", "ts": ts, "dur": dur, "pid": 7, "tid": tid} + + +EVENTS = [ + {"name": "process_name", "ph": "M", "ts": 0, "pid": 7, "tid": 0, "args": {"name": "AnyPS5"}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 1, "args": {"name": "queue worker 0x0"}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 2, "args": {"name": "presenter"}}, + complete(1, "Driver.WorkerWait", 0, 4000), + complete(1, "Driver.Packet", 4000, 5000), + complete(1, "Driver.Packet.memory_wait", 5000, 2000), + complete(1, "Driver.Packet", 12000, 16000), + complete(1, "Driver.Packet.draw", 13000, 7000), + complete(2, "Driver.Present", 0, 2000), + complete(2, "Driver.Present.inflight_wait", 500, 1000), + complete(2, "Driver.Present", 10000, 16000), + complete(2, "Driver.Present.inflight_wait", 10500, 14500), + {"name": "queue worker 0x0 folded", "ph": "C", "ts": 0, "pid": 7, "tid": 1, "args": {"ms": 1.5}}, + {"name": "present", "ph": "i", "ts": 0, "pid": 7, "tid": 2, "s": "g", "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "pid": 7, "tid": 2, "s": "g", "args": {"present": 601}}, + {"name": "present", "ph": "i", "ts": 30000, "pid": 7, "tid": 2, "s": "g", "args": {"present": 602}}, +] + +SUMMARY = {"commit": "abc", "dirty": False, "env": {"APS5_TRACE_FRAMES": "600:3"}, "gpu": "GPU", "driver": "Driver 1", "os": "OS 1", + "resolution": "1920x1080", "window": {"spec": "frames 600:3", "first_present": 600, "ms": 40.0}, "presents": 3, + "frame_ms": {"p50": 10.0, "p90": 20.0, "p99": 20.0, "max": 20.0}, "gpu_busy_ms_per_present": 5.0, + "worker_busy_pct": {"queue worker 0x0": 50.0}, "top_phases_ms_per_frame": {"Driver.Packet": 7.0}, "trace_dropped": False, + "trace": "aps5-trace-abc-7.json.gz"} + +STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll+0x1;a.prx+0x10;a.prx+0x20 3\nntdll.dll+0x1;a.prx+0x10;a.prx+0x30 1\n[truncated];a.prx+0x40;a.prx+0x30 1\n" + + +class TraceDigestTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + root = Path(self.directory.name) + self.trace = root / "aps5-trace-abc-7.json.gz" + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": EVENTS}, handle) + (root / "aps5-summary.jsonl").write_text(json.dumps(dict(SUMMARY, trace="other.json.gz")) + "\n" + json.dumps(SUMMARY) + "\n", encoding="utf-8") + self.stacks = root / "worker.collapsed.txt" + self.stacks.write_text(STACKS, encoding="utf-8") + + def tearDown(self): + self.directory.cleanup() + + def result(self): + return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), + trace_digest.load_stacks(self.stacks), worst=2, top=3) + + def test_summary_is_the_line_of_this_trace(self): + self.assertEqual(self.result()["summary"]["trace"], "aps5-trace-abc-7.json.gz") + + def test_frames_end_at_the_window(self): + result = self.result() + self.assertEqual(result["frames"], 3) + self.assertAlmostEqual(result["window_ms"], 40.0) + + def test_slowest_frame_lists_its_slices(self): + slowest = self.result()["slowest_frames"] + self.assertEqual([frame["present"] for frame in slowest], [601, 600]) + self.assertAlmostEqual(slowest[0]["ms"], 20.0) + largest = {(entry["name"], entry["thread"]): entry["ms"] for entry in slowest[0]["slices"]} + self.assertAlmostEqual(largest[("Driver.Packet", "queue worker 0x0")], 16.0) + self.assertAlmostEqual(largest[("Driver.Present", "presenter")], 16.0) + self.assertAlmostEqual(largest[("Driver.Present.inflight_wait", "presenter")], 14.5) + + def test_threads_split_busy_and_wait(self): + threads = {thread["thread"]: thread for thread in self.result()["threads"]} + worker = threads["queue worker 0x0"] + self.assertAlmostEqual(worker["wait_ms_per_frame"], 6.0 / 3) + self.assertAlmostEqual(worker["busy_ms_per_frame"], 19.0 / 3) + self.assertAlmostEqual(worker["untraced_ms_per_frame"], 15.0 / 3) + self.assertAlmostEqual(worker["folded_ms_per_frame"], 1.5 / 3) + self.assertEqual(worker["top"][0]["name"], "Driver.Packet") + presenter = threads["presenter"] + self.assertAlmostEqual(presenter["wait_ms_per_frame"], 15.5 / 3) + self.assertAlmostEqual(presenter["busy_ms_per_frame"], 2.5 / 3) + + def test_largest_slices_per_frame(self): + largest = self.result()["top_slices_ms_per_frame"] + self.assertEqual(largest[0]["name"], "Driver.Packet") + self.assertAlmostEqual(largest[0]["ms"], 21.0 / 3) + + def test_longest_waits(self): + waits = self.result()["longest_waits"] + self.assertEqual((waits[0]["name"], waits[0]["thread"], waits[0]["present"]), ("Driver.Present.inflight_wait", "presenter", 601)) + self.assertAlmostEqual(waits[0]["ms"], 14.5) + self.assertEqual(waits[1]["name"], "Driver.WorkerWait") + + def test_stacks_self_and_total(self): + stacks = self.result()["stacks"] + self.assertEqual(stacks["samples"], 5) + self.assertAlmostEqual(stacks["truncated_pct"], 20.0) + self.assertEqual(stacks["self"][0], {"frame": "a.prx+0x20", "pct": 60.0}) + total = {entry["frame"]: entry["pct"] for entry in stacks["total"]} + self.assertEqual(stacks["common_frames"], 2) + self.assertNotIn("a.prx+0x10", total) + self.assertEqual(total, {"a.prx+0x20": 60.0, "a.prx+0x30": 40.0, "a.prx+0x40": 20.0}) + + def test_command_line_markdown_and_json(self): + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--stacks", str(self.stacks)]), 0) + text = output.getvalue() + for heading in ("**Slowest frames**", "**Threads**", "**Largest slices**", "**Longest waits**", "**Worker stacks**"): + self.assertIn(heading, text) + self.assertLess(len(text.encode("utf-8")), 5000) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.trace), "--json"]) + self.assertEqual(json.loads(output.getvalue())["frames"], 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py new file mode 100644 index 0000000000..99011db35e --- /dev/null +++ b/tools/trace_digest.py @@ -0,0 +1,222 @@ +import argparse +import bisect +import collections +import gzip +import json +import sys +from pathlib import Path + +DESCRIPTION = """Digest of an APS5_TRACE_FRAMES / APS5_TRACE_SECONDS capture (aps5-trace-*.json.gz) for an issue or a +pull request: the summary line, the slowest frames with their largest slices, the busy and wait time of each +thread, the largest slices per frame, the longest waits and, with --stacks, the hottest functions of an +APS5_SAMPLE_WORKER collapsed-stack file. The summary line is read from aps5-summary.jsonl next to the trace +unless --summary names another file.""" + + +def is_wait(name): + return "wait" in name.rsplit(".", 1)[-1].lower() + + +class Trace: + def __init__(self, document): + self.threads = {} + self.slices = collections.defaultdict(list) + self.presents = [] + self.folded = collections.defaultdict(float) + for event in document["traceEvents"]: + phase = event.get("ph") + if phase == "M" and event.get("name") == "thread_name": + self.threads[event["tid"]] = event["args"]["name"] + elif phase == "X": + self.slices[event["tid"]].append([float(event["ts"]), float(event["dur"]), event["name"], 0, False]) + elif phase == "i" and event.get("name") == "present": + self.presents.append((float(event["ts"]), event.get("args", {}).get("present"))) + elif phase == "C": + self.folded[event["tid"]] += sum(float(value) for value in event.get("args", {}).values()) + self.presents.sort() + for slices in self.slices.values(): + slices.sort(key=lambda entry: (entry[0], -entry[1])) + stack = [] + for entry in slices: + while stack and entry[0] >= stack[-1][0] + stack[-1][1]: + stack.pop() + entry[3] = len(stack) + entry[4] = any(is_wait(parent[2]) for parent in stack) + stack.append(entry) + + def name(self, tid): + return self.threads.get(tid, f"tid {tid}") + + def frames(self, end): + starts = [ts for ts, _ in self.presents] + if end is None: + end = starts[-1] if starts else 0.0 + bounds = starts + ([end] if not starts or end > starts[-1] else []) + return [(index, bounds[index], bounds[index + 1], self.presents[index][1]) for index in range(len(bounds) - 1)] + + +def load_trace(path): + opener = gzip.open if str(path).endswith(".gz") else open + with opener(path, "rt", encoding="utf-8") as handle: + return Trace(json.load(handle)) + + +def load_summary(path, trace_path): + candidate = Path(path) if path else Path(trace_path).with_name("aps5-summary.jsonl") + if not candidate.exists(): + return None + lines = [json.loads(line) for line in candidate.read_text(encoding="utf-8").splitlines() if line.strip()] + for line in reversed(lines): + if path or line.get("trace") == Path(trace_path).name: + return line + return None + + +def load_stacks(path): + stacks = [] + for line in Path(path).read_text(encoding="utf-8").splitlines(): + if line.startswith("#") or not line.strip(): + continue + frames, _, count = line.rpartition(" ") + stacks.append((frames.split(";"), int(count))) + return stacks + + +def digest(trace, summary=None, stacks=None, worst=5, top=10): + window_ms = summary["window"]["ms"] if summary else None + frames = trace.frames(window_ms * 1000.0 if window_ms is not None else None) + span_us = frames[-1][2] - frames[0][1] if frames else 0.0 + count = max(len(frames), 1) + result = {"summary": summary, "frames": len(frames), "window_ms": span_us / 1000.0} + + slowest = [] + for index, begin, end, present in sorted(frames, key=lambda frame: frame[1] - frame[2])[:worst]: + inside = collections.Counter() + for tid, slices in trace.slices.items(): + for ts, dur, name, _, _ in slices: + overlap = min(ts + dur, end) - max(ts, begin) + if overlap > 0: + inside[(name, trace.name(tid))] += overlap + slowest.append({"present": present, "ms": (end - begin) / 1000.0, "at_ms": begin / 1000.0, + "slices": [{"name": name, "thread": thread, "ms": us / 1000.0} for (name, thread), us in inside.most_common(4)]}) + result["slowest_frames"] = slowest + + threads = [] + totals = collections.Counter() + waits = [] + for tid, slices in sorted(trace.slices.items()): + traced = sum(dur for _, dur, _, depth, _ in slices if depth == 0) + waited = sum(dur for _, dur, name, _, nested in slices if is_wait(name) and not nested) + leaders = collections.Counter() + for ts, dur, name, depth, nested in slices: + totals[name] += dur + if depth == 0 and not is_wait(name): + leaders[name] += dur + if is_wait(name) and not nested: + waits.append((dur, name, trace.name(tid), ts)) + threads.append({"thread": trace.name(tid), "busy_ms_per_frame": (traced - waited) / 1000.0 / count, + "wait_ms_per_frame": waited / 1000.0 / count, + "untraced_ms_per_frame": max(span_us - traced, 0.0) / 1000.0 / count, + "folded_ms_per_frame": trace.folded.get(tid, 0.0) / count, + "busy_pct": 100.0 * (traced - waited) / span_us if span_us else 0.0, + "wait_pct": 100.0 * waited / span_us if span_us else 0.0, + "top": [{"name": name, "ms_per_frame": us / 1000.0 / count} for name, us in leaders.most_common(2)]}) + result["threads"] = threads + result["top_slices_ms_per_frame"] = [{"name": name, "ms": us / 1000.0 / count} for name, us in totals.most_common(top)] + starts = [ts for ts, _ in trace.presents] + result["longest_waits"] = [{"name": name, "thread": thread, "ms": dur / 1000.0, "at_ms": ts / 1000.0, + "present": trace.presents[bisect.bisect_right(starts, ts) - 1][1] if starts and ts >= starts[0] else None} + for dur, name, thread, ts in sorted(waits, reverse=True)[:top]] + + if stacks is not None: + samples = sum(count for _, count in stacks) + own = collections.Counter() + inclusive = collections.Counter() + truncated = 0 + common = None + for frames_of, number in stacks: + if frames_of[0] == "[truncated]": + truncated += number + frames_of = frames_of[1:] + elif frames_of: + common = set(frames_of) if common is None else common & set(frames_of) + if not frames_of: + continue + own[frames_of[-1]] += number + for frame in set(frames_of): + inclusive[frame] += number + common = common or set() + for frame in common: + del inclusive[frame] + share = lambda value: 100.0 * value / samples if samples else 0.0 + result["stacks"] = {"samples": samples, "truncated_pct": share(truncated), "common_frames": len(common), + "self": [{"frame": frame, "pct": share(value)} for frame, value in own.most_common(top)], + "total": [{"frame": frame, "pct": share(value)} for frame, value in inclusive.most_common(top)]} + return result + + +def markdown(result): + lines = [] + summary = result["summary"] + if summary: + frame = summary["frame_ms"] + lines.append(f"**{summary['window']['spec']}** from present {summary['window']['first_present']}: {summary['presents']} presents, " + f"frame p50 {frame['p50']:.1f} / p90 {frame['p90']:.1f} / p99 {frame['p99']:.1f} / max {frame['max']:.1f} ms, " + f"GPU busy {summary['gpu_busy_ms_per_present']:.1f} ms per present") + lines.append(f"{summary['gpu']}, {summary['driver']}, {summary['os']}, {summary['resolution']}, commit {summary['commit']}" + f"{' (dirty)' if summary.get('dirty') else ''}, dropped: {'yes' if summary['trace_dropped'] else 'no'}") + lines.append("") + lines.append("```json") + lines.append(json.dumps(summary, separators=(",", ":"))) + lines.append("```") + else: + lines.append(f"{result['frames']} frames over {result['window_ms']:.1f} ms (no summary line found)") + lines.append("") + lines.append("**Slowest frames** (largest slices inside each, inclusive)") + lines.append("") + for frame in result["slowest_frames"]: + parts = ", ".join(f"{entry['name']} ({entry['thread']}) {entry['ms']:.1f}" for entry in frame["slices"]) + lines.append(f"- present {frame['present']}: {frame['ms']:.1f} ms at {frame['at_ms']:.0f} ms: {parts}") + lines.append("") + lines.append("**Threads** (ms per frame)") + lines.append("") + lines.append("| thread | busy | wait | untraced | in slices < 20 us | largest |") + lines.append("|---|---|---|---|---|---|") + for thread in result["threads"]: + largest = ", ".join(f"{entry['name']} {entry['ms_per_frame']:.1f}" for entry in thread["top"]) + lines.append(f"| {thread['thread']} | {thread['busy_ms_per_frame']:.1f} ({thread['busy_pct']:.0f} %) | {thread['wait_ms_per_frame']:.1f} ({thread['wait_pct']:.0f} %) " + f"| {thread['untraced_ms_per_frame']:.1f} | {thread['folded_ms_per_frame']:.1f} | {largest} |") + lines.append("") + lines.append("**Largest slices** (ms per frame, inclusive, all threads): " + ", ".join(f"{entry['name']} {entry['ms']:.1f}" for entry in result["top_slices_ms_per_frame"])) + lines.append("") + lines.append("**Longest waits**") + lines.append("") + for wait in result["longest_waits"]: + lines.append(f"- {wait['name']} ({wait['thread']}) {wait['ms']:.1f} ms at {wait['at_ms']:.0f} ms, present {wait['present']}") + if "stacks" in result: + stacks = result["stacks"] + lines.append("") + lines.append(f"**Worker stacks** ({stacks['samples']} samples, {stacks['truncated_pct']:.1f} % truncated; total leaves out the {stacks['common_frames']} frames every complete stack shares)") + lines.append("") + lines.append("- self: " + ", ".join(f"{entry['frame']} {entry['pct']:.1f} %" for entry in stacks["self"])) + lines.append("- total: " + ", ".join(f"{entry['frame']} {entry['pct']:.1f} %" for entry in stacks["total"])) + return "\n".join(lines) + "\n" + + +def main(arguments=None): + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json)") + parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") + parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt)") + parser.add_argument("--frames", type=int, default=5, help="slowest frames to list (default 5)") + parser.add_argument("--top", type=int, default=10, help="entries per list (default 10)") + parser.add_argument("--json", action="store_true", help="print the digest as JSON") + options = parser.parse_args(arguments) + result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), + load_stacks(options.stacks) if options.stacks else None, options.frames, options.top) + sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())